def inject_summary_fields(sender, **kw): SiteSummary = rt.models.working.SiteSummary UserSummary = rt.models.working.UserSummary WorkSite = rt.models.tickets.Site Ticket = dd.plugins.working.ticket_model for t in ReportingTypes.get_list_items(): k = t.name + '_hours' dd.inject_field(SiteSummary, k, dd.DurationField(t.text, null=True, blank=True)) dd.inject_field(UserSummary, k, dd.DurationField(t.text, null=True, blank=True)) dd.inject_field(Ticket, k, dd.DurationField(t.text, null=True, blank=True)) def make_getter(t): k = t.name + '_hours' def getter(obj, ar): qs = SiteSummary.objects.filter(master=obj, year__isnull=True) d = qs.aggregate(**{k: models.Sum(k)}) n = d[k] return n return getter dd.inject_field( WorkSite, k, dd.VirtualField(dd.DurationField(t.text), make_getter(t))) if False: # removed 20181211 because useless for ts in TicketStates.get_list_items(): k = ts.get_summary_field() if k is not None: dd.inject_field(SiteSummary, k, models.IntegerField(ts.text)) def make_getter(ts): k = ts.get_summary_field() def getter(obj, ar): if ar is None: return '' qs = SiteSummary.objects.filter(master=obj) d = qs.aggregate(**{k: models.Sum(k)}) n = d[k] if n == 0: return '' sar = rt.models.tickets.TicketsBySite.request( obj, param_values=dict(state=ts, show_active=None)) # n = sar.get_total_count() url = ar.renderer.request_handler(sar) if url is None: return str(n) return E.a(str(n), href='javascript:' + url) return getter dd.inject_field( WorkSite, k, dd.VirtualField(dd.DisplayField(ts.text), make_getter(ts)))
def setup_columns(cls): def w(ut): def func(fld, obj, ar): if isinstance(ut.role, obj): return "☑" return "" return func names = [] for ut in UserTypes.get_list_items(): name = "ut" + ut.value # vf = dd.VirtualField( # models.BooleanField(str(ut.value)), w(ut)) vf = dd.VirtualField(dd.DisplayField(str(ut.value)), w(ut)) cls.add_virtual_field(name, vf) names.append(name + ":3") # cls.column_names = "name:20 description:40 " + ' '.join(names) cls.column_names = "name:20 " + ' '.join(names)
class Event(Component, Ended, Assignable, TypedPrintable, Mailable, Postable): """A **calendar entry** is a lapse of time to be visualized in a calendar. .. attribute:: start_date .. attribute:: start_time .. attribute:: end_date .. attribute:: end_time These four fields define the duration of this entry. Only :attr:`start_date` is mandatory. If :attr:`end_date` is the same as :attr:`start_date`, then it is preferrable to leave it empty. .. attribute:: summary A one-line descriptive text. .. attribute:: description A longer descriptive text. .. attribute:: user The responsible user. .. attribute:: assigned_to Another user who is expected to take responsibility for this event. See :attr:`lino.modlib.users.mixins.Assignable.assigned_to`. .. attribute:: event_type The type of this event. Every calendar event should have this field pointing to a given :class:`EventType`, which holds extended configurable information about this event. .. attribute:: state The state of this entry. The state can change according to rules defined by the workflow, that's why we sometimes refer to it as the life cycle. .. attribute:: when_html Shows the date and time of the event with a link that opens all events on that day (:class:`EventsByDay <lino_xl.lib.cal.ui.EventsByDay>`). .. attribute:: show_conflicting A :class:`ShowSlaveTable <lino.core.actions.ShowSlaveTable>` button which opens the :class:`ConflictingEvents <lino_xl.lib.cal.ui.ConflictingEvents>` table for this event. """ class Meta: app_label = 'cal' abstract = dd.is_abstract_model(__name__, 'Event') # abstract = True verbose_name = _("Calendar entry") verbose_name_plural = _("Calendar entries") # verbose_name = pgettext("cal", "Event") # verbose_name_plural = pgettext("cal", "Events") update_guests = UpdateGuests() update_events = UpdateEventsByEvent() event_type = models.ForeignKey('cal.EventType', blank=True, null=True) transparent = models.BooleanField( _("Transparent"), default=False, help_text=_("""\ Indicates that this Event shouldn't prevent other Events at the same time.""")) room = dd.ForeignKey('cal.Room', null=True, blank=True) # iCal:LOCATION priority = models.ForeignKey(Priority, null=True, blank=True) state = EventStates.field( default=EventStates.suggested.as_callable) # iCal:STATUS all_day = ExtAllDayField(_("all day")) move_next = MoveEventNext() show_conflicting = dd.ShowSlaveTable(ConflictingEvents) def strftime(self): if not self.start_date: return '' d = self.start_date.strftime(settings.SITE.date_format_strftime) if self.start_time: t = self.start_time.strftime( settings.SITE.time_format_strftime) return "%s %s" % (d, t) else: return d def __str__(self): if self.pk: s = self._meta.verbose_name + " #" + str(self.pk) else: s = _("Unsaved %s") % self._meta.verbose_name if self.summary: s += " " + self.summary when = self.strftime() if when: s += " (%s)" % when return s def has_conflicting_events(self): """Whether this event has any conflicting events. This is roughly equivalent to asking whether :meth:`get_conflicting_events()` returns more than 0 events. Except when this event's type tolerates more than one events at the same time. """ qs = self.get_conflicting_events() if qs is None: return False if self.event_type is not None: if qs.filter(event_type__all_rooms=True).count() > 0: return True n = self.event_type.max_conflicting - 1 else: n = 0 return qs.count() > n def get_conflicting_events(self): """ Return a QuerySet of Events that conflict with this one. Must work also when called on an unsaved instance. May return None to indicate an empty queryset. Applications may override this to add specific conditions. """ if self.transparent: return if self.state.transparent: return # return False # Event = dd.resolve_model('cal.Event') # ot = ContentType.objects.get_for_model(RecurrentEvent) qs = self.__class__.objects.filter(transparent=False) end_date = self.end_date or self.start_date flt = Q(start_date=self.start_date, end_date__isnull=True) flt |= Q(end_date__isnull=False, start_date__lte=self.start_date, end_date__gte=end_date) if end_date == self.start_date: if self.start_time and self.end_time: # the other starts before me and ends after i started c1 = Q(start_time__lte=self.start_time, end_time__gt=self.start_time) # the other ends after me and started before i ended c2 = Q(end_time__gte=self.end_time, start_time__lt=self.end_time) # the other is full day c3 = Q(end_time__isnull=True, start_time__isnull=True) flt &= (c1 | c2 | c3) qs = qs.filter(flt) # saved events don't conflict with themselves: if self.id is not None: qs = qs.exclude(id=self.id) # automatic events never conflict with other generated events # of same owner. Rule needed for update_events. if self.auto_type: qs = qs.exclude( # auto_type=self.auto_type, auto_type__isnull=False, owner_id=self.owner_id, owner_type=self.owner_type) # transparent events (cancelled or omitted) usually don't # cause a conflict with other events (e.g. a holiday), except # if the other event has the same owner (because a cancelled # course lesson should not tolerate another lesson on the same # date). qs = qs.filter( Q(state__in=EventStates.filter(transparent=False)) | Q( owner_id=self.owner_id, owner_type=self.owner_type)) if self.room is None: # a non-holiday event without room conflicts with a # holiday event if self.event_type is None or not self.event_type.all_rooms: qs = qs.filter(event_type__all_rooms=True) else: # other event in the same room c1 = Q(room=self.room) # other event locks all rooms (e.g. holidays) # c2 = Q(room__isnull=False, event_type__all_rooms=True) c2 = Q(event_type__all_rooms=True) qs = qs.filter(c1 | c2) if self.user is not None: if self.event_type is not None: if self.event_type.locks_user: # c1 = Q(event_type__locks_user=False) # c2 = Q(user=self.user) # qs = qs.filter(c1|c2) qs = qs.filter(user=self.user, event_type__locks_user=True) # qs = Event.objects.filter(flt,owner_type=ot) # if we.start_date.month == 7: # print 20131011, self, we.start_date, qs.count() # print 20131025, qs.query return qs def is_fixed_state(self): return self.state.fixed # return self.state in EventStates.editable_states def is_user_modified(self): return self.state != EventStates.suggested def after_ui_save(self, ar, cw): super(Event, self).after_ui_save(ar, cw) self.update_guests.run_from_code(ar) def suggest_guests(self): """Yield the list of Guest instances to be added to this Event. This method is called from :meth:`update_guests`. """ if self.owner: for obj in self.owner.suggest_cal_guests(self): yield obj def get_event_summary(event, ar): """How this event should be summarized in contexts where possibly another user is looking (i.e. currently in invitations of guests, or in the extensible calendar panel). """ # from django.utils.translation import ugettext as _ s = event.summary # if event.owner_id: # s += " ({0})".format(event.owner) if event.user is not None and event.user != ar.get_user(): if event.access_class == AccessClasses.show_busy: s = _("Busy") s = event.user.username + ': ' + unicode(s) elif settings.SITE.project_model is not None \ and event.project is not None: s += " " + unicode(_("with")) + " " + unicode(event.project) if event.state: s = ("(%s) " % unicode(event.state)) + s n = event.guest_set.all().count() if n: s = ("[%d] " % n) + s return s def before_ui_save(self, ar, **kw): """Mark the event as "user modified" by setting a default state. This is important because EventGenerators may not modify any user-modified Events. """ # logger.info("20130528 before_ui_save") if self.state is EventStates.suggested: self.state = EventStates.draft return super(Event, self).before_ui_save(ar, **kw) def on_create(self, ar): self.event_type = ar.user.event_type or \ settings.SITE.site_config.default_event_type self.start_date = settings.SITE.today() self.start_time = timezone.now().time() # 20130722 e.g. CreateClientEvent sets it explicitly if self.assigned_to is None: self.assigned_to = ar.subst_user super(Event, self).on_create(ar) # def on_create(self,ar): # self.start_date = settings.SITE.today() # self.start_time = datetime.datetime.now().time() # ~ # default user is almost the same as for UserAuthored # ~ # but we take the *real* user, not the "working as" # if self.user_id is None: # u = ar.user # if u is not None: # self.user = u # super(Event,self).on_create(ar) def get_postable_recipients(self): """return or yield a list of Partners""" if self.project: if isinstance(self.project, dd.plugins.cal.partner_model): yield self.project for g in self.guest_set.all(): yield g.partner # if self.user.partner: # yield self.user.partner def get_mailable_type(self): return self.event_type def get_mailable_recipients(self): if self.project: if isinstance(self.project, dd.plugins.cal.partner_model): yield ('to', self.project) for g in self.guest_set.all(): yield ('to', g.partner) if self.user.partner: yield ('cc', self.user.partner) # def get_mailable_body(self,ar): # return self.description @dd.displayfield(_("When")) def when_text(self, ar): txt = when_text(self.start_date, self.start_time) if self.end_date and self.end_date != self.start_date: txt += "-" + when_text(self.end_date, self.end_time) return txt @dd.displayfield(_("When")) # def linked_date(self, ar): def when_html(self, ar): if ar is None: return '' EventsByDay = settings.SITE.modules.cal.EventsByDay txt = when_text(self.start_date, self.start_time) return EventsByDay.as_link(ar, self.start_date, txt) @dd.displayfield(_("Link URL")) def url(self, ar): return 'foo' @dd.virtualfield(dd.DisplayField(_("Reminder"))) def reminder(self, request): return False # reminder.return_type = dd.DisplayField(_("Reminder")) def get_calendar(self): """ Returns the Calendar which contains this event, or None if no subscription is found. Needed for ext.ensible calendar panel. The default implementation returns None. Override this if your app uses Calendars. """ # for sub in Subscription.objects.filter(user=ar.get_user()): # if sub.contains_event(self): # return sub return None @dd.virtualfield(models.ForeignKey('cal.Calendar')) def calendar(self, ar): return self.get_calendar() def get_print_language(self): # if settings.SITE.project_model is not None and self.project: if self.project: return self.project.get_print_language() if self.user: return self.user.language return settings.SITE.get_default_language() @classmethod def get_default_table(cls): return OneEvent @classmethod def on_analyze(cls, lino): cls.DISABLED_AUTO_FIELDS = dd.fields_list(cls, "summary") super(Event, cls).on_analyze(lino) def auto_type_changed(self, ar): """When the number has changed, we must update the summary.""" if self.auto_type: self.summary = self.owner.update_cal_summary(self.auto_type)
class Event(Component, Ended, mixins.TypedPrintable, Mailable, Postable): """A calendar event is a lapse of time to be visualized in a calendar. .. attribute:: user The responsible user. .. attribute:: assigned_to This field is usually empty. Setting it to another user means "I am not fully responsible for this event". This will cause the other user to see this event in his :class:`MyAssignedEvents` table. This field is cleared when somebody calls :class:`TakeEvent` on the event. .. attribute:: event_type The type of this event. Every calendar event should have this field pointing to a given :class:`EventType`, which holds extended configurable information about this event. .. attribute:: linked_date Shows the date and time of the event with a link that opens all events on that day (cal.EventsByDay) """ class Meta: app_label = 'cal' abstract = dd.is_abstract_model(__name__, 'Event') #~ abstract = True verbose_name = pgettext("cal", "Event") verbose_name_plural = pgettext("cal", "Events") update_guests = UpdateGuests() event_type = models.ForeignKey('cal.EventType', blank=True, null=True) transparent = models.BooleanField(_("Transparent"), default=False, help_text=_("""\ Indicates that this Event shouldn't prevent other Events at the same time.""")) room = dd.ForeignKey('cal.Room', null=True, blank=True) # iCal:LOCATION priority = models.ForeignKey(Priority, null=True, blank=True) state = EventStates.field( default=EventStates.suggested.as_callable) # iCal:STATUS all_day = ExtAllDayField(_("all day")) assigned_to = dd.ForeignKey(settings.SITE.user_model, verbose_name=_("Assigned to"), related_name="cal_events_assigned", blank=True, null=True) move_next = MoveEventNext() def strftime(self): if not self.start_date: return '' d = self.start_date.strftime(settings.SITE.date_format_strftime) if self.start_time: t = self.start_time.strftime(settings.SITE.time_format_strftime) return "%s %s" % (d, t) else: return d def __unicode__(self): if self.pk: s = self._meta.verbose_name + " #" + str(self.pk) else: s = _("Unsaved %s") % self._meta.verbose_name if self.summary: s += " " + self.summary when = self.strftime() if when: s += " (%s)" % when return s def has_conflicting_events(self): qs = self.get_conflicting_events() if qs is None: return False if self.event_type is not None: n = self.event_type.max_conflicting - 1 else: n = 0 return qs.count() > n def get_conflicting_events(self): """ Return a QuerySet of Events that conflict with this one. Must work also when called on an unsaved instance. May return None to indicate an empty queryset. Applications may override this to add specific conditions. """ if self.transparent: return #~ return False #~ Event = dd.resolve_model('cal.Event') #~ ot = ContentType.objects.get_for_model(RecurrentEvent) qs = self.__class__.objects.filter(transparent=False) end_date = self.end_date or self.start_date flt = Q(start_date=self.start_date, end_date__isnull=True) flt |= Q(end_date__isnull=False, start_date__lte=self.start_date, end_date__gte=end_date) if end_date == self.start_date: if self.start_time and self.end_time: # the other starts before me and ends after i started c1 = Q(start_time__lte=self.start_time, end_time__gt=self.start_time) # the other ends after me and started before i ended c2 = Q(end_time__gte=self.end_time, start_time__lt=self.end_time) # the other is full day c3 = Q(end_time__isnull=True, start_time__isnull=True) flt &= (c1 | c2 | c3) qs = qs.filter(flt) if self.id is not None: # don't conflict with myself qs = qs.exclude(id=self.id) # generated events never conflict with other generated events # of same owner. Rule needed for update_events. if self.auto_type is not None: qs = qs.exclude( # auto_type=self.auto_type, owner_id=self.owner_id, owner_type=self.owner_type) if self.room is not None: # other event in the same room c1 = Q(room=self.room) # other event locks all rooms (e.h. holidays) c2 = Q(event_type__all_rooms=True) qs = qs.filter(c1 | c2) if self.user is not None: if self.event_type is not None: if self.event_type.locks_user: #~ c1 = Q(event_type__locks_user=False) #~ c2 = Q(user=self.user) #~ qs = qs.filter(c1|c2) qs = qs.filter(user=self.user, event_type__locks_user=True) #~ qs = Event.objects.filter(flt,owner_type=ot) #~ if we.start_date.month == 7: #~ print 20131011, self, we.start_date, qs.count() #~ print 20131025, qs.query return qs def is_fixed_state(self): return self.state.fixed #~ return self.state in EventStates.editable_states def is_user_modified(self): return self.state != EventStates.suggested def after_ui_create(self, ar): super(Event, self).after_ui_create(ar) self.update_guests.run_from_code(ar) def after_ui_save(self, ar, cw): super(Event, self).after_ui_save(ar, cw) self.update_guests.run_from_code(ar) def suggest_guests(self): """Yield the list of Guest instances to be added to this Event. This method is called from :meth:`update_guests`. """ if self.owner: for obj in self.owner.suggest_cal_guests(self): yield obj def get_event_summary(event, ar): """How this event should be summarized in contexts where possibly another user is looking (i.e. currently in invitations of guests, or in the extensible calendar panel). """ #~ from django.utils.translation import ugettext as _ s = event.summary if event.user is not None and event.user != ar.get_user(): if event.access_class == AccessClasses.show_busy: s = _("Busy") s = event.user.username + ': ' + unicode(s) elif settings.SITE.project_model is not None \ and event.project is not None: s += " " + unicode(_("with")) + " " + unicode(event.project) if event.state: s = ("(%s) " % unicode(event.state)) + s n = event.guest_set.all().count() if n: s = ("[%d] " % n) + s return s def before_ui_save(self, ar, **kw): """Mark the event as "user modified" by setting a default state. This is important because EventGenerators may not modify any user-modified Events. """ #~ logger.info("20130528 before_ui_save") if self.state is EventStates.suggested: self.state = EventStates.draft return super(Event, self).before_ui_save(ar, **kw) def on_create(self, ar): self.event_type = ar.user.event_type or \ settings.SITE.site_config.default_event_type self.start_date = settings.SITE.today() self.start_time = datetime.datetime.now().time() # 20130722 e.g. CreateClientEvent sets it explicitly if self.assigned_to is None: self.assigned_to = ar.subst_user super(Event, self).on_create(ar) #~ def on_create(self,ar): #~ self.start_date = settings.SITE.today() #~ self.start_time = datetime.datetime.now().time() # ~ # default user is almost the same as for UserAuthored # ~ # but we take the *real* user, not the "working as" #~ if self.user_id is None: #~ u = ar.user #~ if u is not None: #~ self.user = u #~ super(Event,self).on_create(ar) def get_postable_recipients(self): """return or yield a list of Partners""" if self.project: if isinstance(self.project, rt.modules.contacts.Partner): yield self.project for g in self.guest_set.all(): yield g.partner #~ if self.user.partner: #~ yield self.user.partner def get_mailable_type(self): return self.event_type def get_mailable_recipients(self): if self.project: if isinstance(self.project, rt.modules.contacts.Partner): yield ('to', self.project) for g in self.guest_set.all(): yield ('to', g.partner) if self.user.partner: yield ('cc', self.user.partner) #~ def get_mailable_body(self,ar): #~ return self.description def get_system_note_recipients(self, request, silent): if self.user != request.user: yield "%s <%s>" % (unicode(self.user), self.user.email) if silent: return for g in self.guest_set.all(): if g.partner.email: yield "%s <%s>" % (unicode(g.partner), g.partner.email) @dd.displayfield(_("When")) def when_text(self, ar): if ar is None: return '' txt = when_text(self.start_date, self.start_time) if self.end_date and self.end_date != self.start_date: txt += "-" + when_text(self.end_date, self.end_time) return txt # return ar.obj2html(self, txt) @dd.displayfield(_("Link URL")) def url(self, ar): return 'foo' @dd.displayfield(_("When")) def linked_date(self, ar): EventsByDay = settings.SITE.modules.cal.EventsByDay txt = when_text(self.start_date, self.start_time) return EventsByDay.as_link(ar, self.start_date, txt) @dd.virtualfield(dd.DisplayField(_("Reminder"))) def reminder(self, request): return False #~ reminder.return_type = dd.DisplayField(_("Reminder")) def get_calendar(self): """ Returns the Calendar which contains this event, or None if no subscription is found. Needed for ext.ensible calendar panel. The default implementation returns None. Override this if your app uses Calendars. """ #~ for sub in Subscription.objects.filter(user=ar.get_user()): #~ if sub.contains_event(self): #~ return sub return None @dd.virtualfield(models.ForeignKey('cal.Calendar')) def calendar(self, ar): return self.get_calendar() def get_print_language(self): if settings.SITE.project_model is not None and self.project: return self.project.get_print_language() return self.user.language @classmethod def get_default_table(cls): return OneEvent @classmethod def on_analyze(cls, lino): cls.DISABLED_AUTO_FIELDS = dd.fields_list(cls, "summary") super(Event, cls).on_analyze(lino)
def set_upload_shortcuts(sender, **kw): """This is the successor for `quick_upload_buttons`.""" # remember that models might have been overridden. UploadType = sender.modules.uploads.UploadType for i in Shortcuts.items(): def f(obj, ar): if obj is None or ar is None: return E.div() try: utype = UploadType.objects.get(shortcut=i) except UploadType.DoesNotExist: return E.div() items = [] target = sender.modules.resolve(i.target) sar = ar.spawn_request(actor=target, master_instance=obj, known_values=dict(type=utype)) # param_values=dict(pupload_type=et)) n = sar.get_total_count() if n == 0: iar = target.insert_action.request_from(sar, master_instance=obj) btn = iar.ar2button( None, _("Upload"), icon_name="page_add", title=_("Upload a file from your PC to the server.")) items.append(btn) elif n == 1: after_show = ar.get_status() obj = sar.data_iterator[0] items.append( sar.renderer.href_button(dd.build_media_url(obj.file.name), _("show"), target='_blank', icon_name='page_go', style="vertical-align:-30%;", title=_( "Open the uploaded file in a " "new browser window"))) after_show.update(record_id=obj.pk) items.append( sar.window_action_button( sar.ah.actor.detail_action, after_show, _("Edit"), icon_name='application_form', title=_("Edit metadata of the uploaded file."))) else: obj = sar.sliced_data_iterator[0] items.append( ar.obj2html(obj, pgettext("uploaded file", "Last"))) btn = sar.renderer.action_button(obj, sar, sar.bound_action, _("All {0} files").format(n), icon_name=None) items.append(btn) return E.div(*join_elems(items, ', ')) vf = dd.VirtualField(dd.DisplayField(i.text), f) dd.inject_field(i.model_spec, i.name, vf)
class Event(Component, Ended, Assignable, TypedPrintable, Mailable, Postable, Publishable): class Meta: app_label = 'cal' abstract = dd.is_abstract_model(__name__, 'Event') # abstract = True verbose_name = _("Calendar entry") verbose_name_plural = _("Calendar entries") # verbose_name = pgettext("cal", "Event") # verbose_name_plural = pgettext("cal", "Events") publisher_location = "cal" publisher_page_template = "cal/event.pub.html" update_guests = UpdateGuests() update_events = UpdateEntriesByEvent() show_today = ShowEntriesByDay('start_date') event_type = dd.ForeignKey('cal.EventType', blank=True, null=True) transparent = models.BooleanField(_("Transparent"), default=False) room = dd.ForeignKey('cal.Room', null=True, blank=True) # priority = dd.ForeignKey(Priority, null=True, blank=True) state = EntryStates.field(default='suggested') all_day = ExtAllDayField(_("all day")) move_next = MoveEntryNext() show_conflicting = dd.ShowSlaveTable(ConflictingEvents) allow_merge_action = False @classmethod def setup_parameters(cls, params): super(Event, cls).setup_parameters(params) params.update( # user=dd.ForeignKey(settings.SITE.user_model, # verbose_name=_("Managed by"), # blank=True, null=True), # event_type=dd.ForeignKey('cal.EventType', blank=True, null=True), # room=dd.ForeignKey('cal.Room', blank=True, null=True), # project=dd.ForeignKey(settings.SITE.project_model, blank=True, null=True), # assigned_to=dd.ForeignKey(settings.SITE.user_model, # verbose_name=_("Assigned to"), # blank=True, null=True), # state=EntryStates.field(blank=True), # unclear = models.BooleanField(_("Unclear events")) observed_event=EventEvents.field(blank=True), show_appointments=dd.YesNo.field(_("Appointments"), blank=True), presence_guest=dd.ForeignKey( dd.plugins.cal.partner_model, verbose_name=_("Guest"), blank=True, null=True) ) # if settings.SITE.project_model: # params['project'].help_text = format_lazy( # _("Show only entries having this {project}."), # project=settings.SITE.project_model._meta.verbose_name) @classmethod def get_simple_parameters(cls): for p in super(Event, cls).get_simple_parameters(): yield p yield 'state' yield 'room' yield 'event_type' @classmethod def get_request_queryset(cls, ar, **filter): qs = super(Event, cls).get_request_queryset(ar, **filter) # print("20200430 cal.Event.get_request_queryset", ar.param_values.project, qs.query) # return cls.calendar_param_filter(qs, ar.param_values) pv = ar.param_values # if pv.user: # qs = qs.filter(user=pv.user) # if pv.assigned_to: # qs = qs.filter(assigned_to=pv.assigned_to) # # if settings.SITE.project_model is not None and pv.project: # qs = qs.filter(project=pv.project) if pv.event_type: # qs = qs.filter(event_type=pv.event_type) pass else: if pv.show_appointments == dd.YesNo.yes: qs = qs.filter(event_type__is_appointment=True) elif pv.show_appointments == dd.YesNo.no: qs = qs.filter(event_type__is_appointment=False) # if pv.state: # qs = qs.filter(state=pv.state) # if pv.room: # qs = qs.filter(room=pv.room) # if pv.observed_event == EventEvents.stable: qs = qs.filter(state__in=set(EntryStates.filter(fixed=True))) elif pv.observed_event == EventEvents.pending: qs = qs.filter(state__in=set(EntryStates.filter(fixed=False))) # multi-day entries should appear on each day if pv.start_date: c1 = Q(start_date__gte=pv.start_date) c2 = Q(start_date__lt=pv.start_date, end_date__isnull=False, end_date__gte=pv.start_date) qs = qs.filter(c1|c2) if pv.end_date: c1 = Q(end_date__isnull=True, start_date__lte=pv.end_date) c2 = Q(end_date__isnull=False, end_date__lte=pv.end_date) qs = qs.filter(c1|c2) qs = qs.filter(Q(end_date__isnull=True, start_date__lte=pv.end_date)|Q(end_date__lte=pv.end_date)) if pv.presence_guest: qs = qs.filter(Q(guest__partner=pv.presence_guest)|Q(event_type__all_rooms=True)) return qs def strftime(self): if not self.start_date: return '' d = self.start_date.strftime(settings.SITE.date_format_strftime) if self.start_time: t = self.start_time.strftime( settings.SITE.time_format_strftime) return "%s %s" % (d, t) else: return d def get_diplay_color(self): if self.room: return self.room.display_color def __str__(self): if self.summary: s = self.summary elif self.event_type: s = str(self.event_type) elif self.pk: s = self._meta.verbose_name + " #" + str(self.pk) else: s = _("Unsaved %s") % self._meta.verbose_name when = self.strftime() if when: s = "{} ({})".format(s, when) # u = self.user # if u is None and self.room: # u = self.room # if u is None: # return '%s object (%s)' % (self.__class__.__name__, self.pk) # u = u.initials or u.username or str(u) # s = "{} ({})".format(s, u) return s # if e.start_time: # t = str(e.start_time)[:5] # else: # t = str(e.event_type) # u = e.user # if u is None: # return "{} {}".format(t, e.room) if e.room else t # u = u.initials or u.username or str(u) # return "{} {}".format(t, u) def duration_veto(obj): if obj.end_date is None: return et = obj.event_type if et is None: return duration = obj.end_date - obj.start_date # print (20161222, duration.days, et.max_days) if et.max_days and duration.days > et.max_days: return _( "Event lasts {0} days but only {1} are allowed.").format( duration.days, et.max_days) def full_clean(self, *args, **kw): super(Event, self).full_clean(*args, **kw) et = self.event_type if et is not None and et.default_duration is not None: assert isinstance(et.default_duration, Duration) dt = self.get_datetime('start') if dt is not None and self.end_time is None: self.set_datetime('end', dt + et.default_duration) else: dt = self.get_datetime('end') if dt is not None and self.start_time is None: self.set_datetime('start', dt - et.default_duration) if et and et.max_days == 1: # avoid "Abandoning with 297 unsaved instances" when migrating data # that was created before the current rules self.end_date = None msg = self.duration_veto() if msg is not None: raise ValidationError(str(msg)) def start_time_changed(self, ar): et = self.event_type start_time = self.get_datetime('start') if start_time is not None \ and et is not None and et.default_duration is not None: dt = start_time + et.default_duration self.set_datetime('end', dt) # self.end_time = str(self.start_time + et.default_duration) # removed because this behaviour is irritating # def end_time_changed(self, ar): # et = self.event_type # end_time = self.get_datetime('end', 'start') # if end_time is not None \ # and et is not None and et.default_duration is not None: # dt = end_time - et.default_duration # self.set_datetime('start', dt) # # self.start_time = str(self.end_time - et.default_duration) def get_change_observers(self, ar=None): # implements ChangeNotifier if not self.is_user_modified(): return for x in super(Event, self).get_change_observers(ar): yield x for u in (self.user, self.assigned_to): if u is not None: yield (u, u.mail_mode) def has_conflicting_events(self): qs = self.get_conflicting_events() if qs is None: return False if self.event_type is not None: if self.event_type.transparent: return False # holidays (all room events) conflict also with events # whose type otherwise would allow conflicting events if qs.filter(event_type__all_rooms=True).count() > 0: return True n = self.event_type.max_conflicting - 1 else: n = 0 # date = self.start_date # if date.day == 9 and date.month == 3: # dd.logger.info("20171130 has_conflicting_events() %s", qs.query) return qs.count() > n def get_conflicting_events(self): if self.transparent: return # if self.event_type is not None and self.event_type.transparent: # return # return False # Event = dd.resolve_model('cal.Event') # ot = ContentType.objects.get_for_model(RecurrentEvent) qs = self.__class__.objects.filter(transparent=False) qs = qs.exclude(event_type__transparent=True) # if self.state.transparent: # # cancelled entries are basically transparent to all # # others. Except if they have an owner, in which case we # # wouldn't want Lino to put another automatic entry at # # that date. # if self.owner_id is None: # return # qs = qs.filter( # owner_id=self.owner_id, owner_type=self.owner_type) end_date = self.end_date or self.start_date flt = Q(start_date=self.start_date, end_date__isnull=True) flt |= Q(end_date__isnull=False, start_date__lte=self.start_date, end_date__gte=end_date) if end_date == self.start_date: if self.start_time and self.end_time: # the other starts before me and ends after i started c1 = Q(start_time__lte=self.start_time, end_time__gt=self.start_time) # the other ends after me and started before i ended c2 = Q(end_time__gte=self.end_time, start_time__lt=self.end_time) # the other is full day c3 = Q(end_time__isnull=True, start_time__isnull=True) flt &= (c1 | c2 | c3) qs = qs.filter(flt) # saved events don't conflict with themselves: if self.id is not None: qs = qs.exclude(id=self.id) # automatic events never conflict with other generated events # of same owner. Rule needed for update_events. if self.auto_type: qs = qs.exclude( # auto_type=self.auto_type, auto_type__isnull=False, owner_id=self.owner_id, owner_type=self.owner_type) # transparent events (cancelled or omitted) usually don't # cause a conflict with other events (e.g. a holiday). But a # cancelled course lesson should not tolerate another lesson # of the same course on the same date. ntstates = EntryStates.filter(transparent=False) if self.owner_id is None: if self.state.transparent: return qs = qs.filter(state__in=ntstates) else: if self.state.transparent: qs = qs.filter( owner_id=self.owner_id, owner_type=self.owner_type) else: qs = qs.filter( Q(state__in=ntstates) | Q( owner_id=self.owner_id, owner_type=self.owner_type)) if self.room is None: # an entry that needs a room but doesn't yet have one, # conflicts with any all-room entry (e.g. a holiday). For # generated entries this list extends to roomed entries of # the same generator. if self.event_type is None or not self.event_type.all_rooms: if self.owner_id is None: qs = qs.filter(event_type__all_rooms=True) else: qs = qs.filter( Q(event_type__all_rooms=True) | Q( owner_id=self.owner_id, owner_type=self.owner_type)) else: # other event in the same room c1 = Q(room=self.room) # other event locks all rooms (e.g. holidays) # c2 = Q(room__isnull=False, event_type__all_rooms=True) c2 = Q(event_type__all_rooms=True) qs = qs.filter(c1 | c2) if self.user is not None: if self.event_type is not None: if self.event_type.locks_user: # c1 = Q(event_type__locks_user=False) # c2 = Q(user=self.user) # qs = qs.filter(c1|c2) qs = qs.filter(user=self.user, event_type__locks_user=True) # qs = Event.objects.filter(flt,owner_type=ot) # if we.start_date.month == 7: # print 20131011, self, we.start_date, qs.count() # print 20131025, qs.query return qs def is_fixed_state(self): return self.state.fixed # return self.state in EntryStates.editable_states def is_user_modified(self): return self.state != EntryStates.suggested def before_ui_save(self, ar, cw): # logger.info("20130528 before_ui_save") if self.state is EntryStates.suggested: self.state = EntryStates.draft super(Event, self).before_ui_save(ar, cw) def on_create(self, ar): if self.event_type is None: # print("20200513", ar.user) self.event_type = ar.user.event_type or \ settings.SITE.site_config.default_event_type self.start_date = self.start_date or settings.SITE.today() self.start_time = timezone.now().time() # see also Assignable.on_create() super(Event, self).on_create(ar) if not settings.SITE.loading_from_dump: # print("20190328 before_ui_save", self.is_user_modified()) if isinstance(self.owner, RecurrenceSet): self.owner.before_auto_event_save(self) if isinstance(self.owner, EventGenerator): self.event_type = self.owner.update_cal_event_type() # def on_create(self,ar): # self.start_date = settings.SITE.today() # self.start_time = datetime.datetime.now().time() # ~ # default user is almost the same as for UserAuthored # ~ # but we take the *real* user, not the "working as" # if self.user_id is None: # u = ar.user # if u is not None: # self.user = u # super(Event,self).on_create(ar) def after_ui_save(self, ar, cw): super(Event, self).after_ui_save(ar, cw) self.update_guests.run_from_code(ar) def before_state_change(self, ar, old, new): super(Event, self).before_state_change(ar, old, new) if new.noauto: self.auto_type = None if new.guest_state and self.event_type_id and self.event_type.force_guest_states: for obj in self.guest_set.exclude(state=new.guest_state): obj.state = new.guest_state obj.full_clean() obj.save() def can_edit_guests_manually(self): # print("20200901", self.state, self.state.fill_guests) if self.state.fill_guests: if self.event_type and self.event_type.fill_presences: return False # print("20200901 yes") return True def suggest_guests(self): done = set() for o in (self.owner, self.project): if isinstance(o, EventGenerator): if o in done: continue done.add(o) for obj in o.suggest_cal_guests(self): yield obj summary_project_template = _("for {project}") summary_show_user = True def get_event_summary(self, ar): # from django.utils.translation import ugettext as _ s = self.summary if self.owner is not None: s = "{} {}".format(self.owner, s) u = self.user if u is not None and u != ar.get_user(): if self.access_class == AccessClasses.show_busy: s = _("Busy") if self.summary_show_user: s = "{} {}".format(u.initials or u.username, s) # n = event.guest_set.all().count() # if n: # s = ("[%d] " % n) + s if self.state.button_text: yield str(self.state.button_text) + " " yield s if self.project is not None: # s += " " + "{} {}".format(self.summary_project_template, self.project).strip() yield " " + self.summary_project_template.format(project=self.project) def get_postable_recipients(self): """return or yield a list of Partners""" if self.project: if isinstance(self.project, dd.plugins.cal.partner_model): yield self.project for g in self.guest_set.all(): yield g.partner # if self.user.partner: # yield self.user.partner def get_mailable_type(self): return self.event_type def get_mailable_recipients(self): if self.project: if isinstance(self.project, dd.plugins.cal.partner_model): yield ('to', self.project) for g in self.guest_set.all(): yield ('to', g.partner) if self.user.partner: yield ('cc', self.user.partner) # def get_mailable_body(self,ar): # return self.description @dd.displayfield(_("When"), sortable_by=['start_date', 'start_time']) def when_text(self, ar): txt = when_text(self.start_date, self.start_time) if self.end_date and self.end_date != self.start_date: txt += "-" + when_text(self.end_date, self.end_time) return txt @dd.displayfield(_("When"), sortable_by=['start_date', 'start_time']) def when_html(self, ar): if ar is None: return '' txt = when_text(self.start_date, self.start_time) if False: # removed 20181106 because it is irritating and # nobody uses it. return rt.models.cal.EntriesByDay.as_link( ar, self.start_date, txt) return self.obj2href(ar, txt) @dd.displayfield(_("Link URL")) def url(self, ar): return 'foo' @dd.virtualfield(dd.DisplayField(_("Reminder"))) def reminder(self, request): return False # reminder.return_type = dd.DisplayField(_("Reminder")) def get_calendar(self): # for sub in Subscription.objects.filter(user=ar.get_user()): # if sub.contains_event(self): # return sub return None @dd.virtualfield(dd.ForeignKey('cal.Calendar')) def calendar(self, ar): return self.get_calendar() def get_print_language(self): # if settings.SITE.project_model is not None and self.project: if self.project: return self.project.get_print_language() if self.user: return self.user.language return settings.SITE.get_default_language() @classmethod def get_default_table(cls): return OneEvent @classmethod def on_analyze(cls, lino): cls.DISABLED_AUTO_FIELDS = dd.fields_list(cls, "summary") super(Event, cls).on_analyze(lino) def auto_type_changed(self, ar): if self.auto_type and self.owner: self.summary = self.owner.update_cal_summary( self.event_type, self.auto_type)
def set_excerpts_actions(sender, **kw): """Installs (1) print management actions on models for which there is an excerpt type and (2) the excerpt shortcut fields defined in :class:`lino_xl.lib.excerpts.choicelists.Shortcuts`. """ # logger.info("20140401 %s.set_attest_actions()", __name__) # in case ExcerptType is overridden ExcerptType = sender.modules.excerpts.ExcerptType Excerpt = sender.modules.excerpts.Excerpt try: etypes = [(obj, obj.content_type) for obj in ExcerptType.objects.all()] except (OperationalError, ProgrammingError, UnresolvedChoice) as e: dd.logger.debug("Failed to set excerpts actions : %s", e) # Happens e.g. when the database has not yet been migrated etypes = [] for atype, ct in etypes: if ct is not None: m = ct.model_class() if m is not None: # e.g. database contains types for # models that existed before but have # been removed an = atype.get_action_name() m.define_action( **{an: CreateExcerpt(atype, six.text_type(atype))}) # dd.logger.info("Added print action to %s", m) # if atype.certifying and not issubclass(m, Certifiable): # m.define_action( # clear_printed=ClearCache()) # An attestable model must also inherit # :class:`lino.mixins.printable.BasePrintable` or some subclass # thereof. for i in Shortcuts.items(): def f(obj, ar): if ar is None: return '' if obj is None: return E.div() try: et = ExcerptType.objects.get(shortcut=i) except ExcerptType.DoesNotExist: return E.div() items = [] if True: sar = ar.spawn(ExcerptsByOwner, master_instance=obj, param_values=dict(excerpt_type=et)) n = sar.get_total_count() if n > 0: ex = sar.sliced_data_iterator[0] items.append(ar.obj2html(ex, _("Last"))) ba = sar.bound_action btn = sar.renderer.action_button(obj, sar, ba, "%s (%d)" % (_("All"), n), icon_name=None) items.append(btn) ia = getattr(obj, et.get_action_name()) btn = ar.instance_action_button(ia, _("Create"), icon_name=None) items.append(btn) else: ot = ContentType.objects.get_for_model(obj.__class__) qs = Excerpt.objects.filter(owner_id=obj.pk, owner_type=ot, excerpt_type=et) if qs.count() > 0: ex = qs[0] txt = ExcerptsByOwner.format_excerpt(ex) items.append(ar.obj2html(ex, txt)) return E.div(*join_elems(items, ', ')) vf = dd.VirtualField(dd.DisplayField(i.text), f) dd.inject_field(i.model_spec, i.name, vf)