class TextFieldTemplate(UserAuthored): """A reusable block of text that can be selected from a text editor to be inserted into the text being edited. """ class Meta(object): verbose_name = _("Text Field Template") verbose_name_plural = _("Text Field Templates") name = models.CharField(_("Designation"), max_length=200) description = dd.RichTextField(_("Description"), blank=True, null=True, format='plain') #~ blank=True,null=True,format='html') # team = dd.ForeignKey( # 'users.Team', blank=True, null=True, # help_text=_("If not empty, then this template " # "is reserved to members of this team.")) text = dd.RichTextField(_("Template Text"), blank=True, null=True, format='html') def __str__(self): return self.name
class Comment( mixins.CreatedModified, UserAuthored, # mixins.Hierarchical, Controllable): """A **comment** is a short text which some user writes about some other database object. .. attribute:: short_text A short "abstract" of your comment. This should not be more than one paragraph. """ ALLOWED_TAGS = ['a', 'b', 'i', 'em'] class Meta: abstract = dd.is_abstract_model(__name__, 'Comment') verbose_name = _("Comment") verbose_name_plural = _("Comments") short_text = dd.RichTextField(_("Short text")) more_text = dd.RichTextField(_("More text"), blank=True) def __unicode__(self): return u'%s #%s' % (self._meta.verbose_name, self.pk) def as_li(self, ar): """Return this comment as a list item. If `bleach <http://bleach.readthedocs.org/en/latest/>`_ is installed, all tags except some will be removed when """ if bleach is None: chunks = [self.short_text] else: chunks = [bleach.clean( self.short_text, tags=self.ALLOWED_TAGS, strip=True)] by = _("{0} by {1}").format( naturaltime(self.created), unicode(self.user)), chunks += [ " (", E.tostring(ar.obj2html(self, by)), ")" ] if self.more_text: chunks.append(" (...)") html = ''.join(chunks) return "<li>" + html + "</li>"
class RecurrentEvent(mixins.BabelNamed, RecurrenceSet, EventGenerator): """An event that recurs at intervals. """ class Meta: app_label = 'cal' verbose_name = _("Recurrent Event") verbose_name_plural = _("Recurrent Events") event_type = models.ForeignKey('cal.EventType', blank=True, null=True) description = dd.RichTextField(_("Description"), blank=True, format='html') def before_auto_event_save(self, obj): if self.end_date and self.end_date != self.start_date: duration = self.end_date - self.start_date obj.end_date = obj.start_date + duration super(RecurrentEvent, self).before_auto_event_save(obj) #~ def on_create(self,ar): #~ super(RecurrentEvent,self).on_create(ar) #~ self.event_type = settings.SITE.site_config.holiday_event_type #~ def __unicode__(self): #~ return self.summary def update_cal_rset(self): return self def update_cal_from(self, ar): return self.start_date def update_cal_calendar(self): return self.event_type def update_cal_summary(self, i): return unicode(self)
class Item(mixins.Sequenced): class Meta: verbose_name = _("Import Filter Item") verbose_name_plural = _("Import Filter Items") filter = dd.ForeignKey(Filter) field = models.CharField(_("Field"), max_length=200) column = models.IntegerField(_("Column")) help_text = dd.RichTextField(_("HelpText"), blank=True, null=True, format='plain') @dd.chooser(simple_values=True) def field_choices(cls, filter): l = [] if filter is not None: model = filter.content_type.model_class() meta = model._meta for f in meta.fields: if not getattr(f, '_lino_babel_field', False): l.append(f.name) l.sort() return l
class Entry(UserAuthored, Controllable, CombinedDateTime): """A blog entry is a short article with a title, published on a given date and time by a given user. """ class Meta: app_label = 'blogs' verbose_name = _("Blog Entry") verbose_name_plural = _("Blog Entries") title = models.CharField(_("Heading"), max_length=200, blank=True) body = dd.RichTextField(_("Body"), blank=True, format='html') pub_date = models.DateField(_("Publication date"), blank=True, null=True) pub_time = models.TimeField(_("Publication time"), blank=True, null=True) entry_type = dd.ForeignKey('blogs.EntryType', blank=True, null=True) language = dd.LanguageField() def __str__(self): return u'%s #%s' % (self._meta.verbose_name, self.pk) def on_create(self, ar): """ Sets the :attr:`pub_date` and :attr:`pub_time` to now. """ if not settings.SITE.loading_from_dump: self.set_datetime('pub', timezone.now()) self.language = ar.get_user().language super(Entry, self).on_create(ar)
class Import(dd.VirtualTable): column_names = 'description obj2unicode' parameters = dict(filter=dd.ForeignKey(Filter), data=dd.RichTextField(_("Data to import"), blank=True, null=True, format='plain')) @classmethod def get_data_rows(self, ar): flt = ar.param_values.filter build = Instantiator(flt.content_type.model_class()).build for ln in ar.param_values.data.splitlines(): ln = ln.strip() if ln: kw = dict() cells = flt.field_sep.split(ln) for item in flt.item_set.all(): if item.column: kw[item.field] = cells[item.column - 1] yield build(**kw) @dd.displayfield(_("Description")) def description(cls, obj, ar): kw = dict() flt = ar.param_values.filter for item in flt.item_set.all(): kw[item.field] = getattr(obj, item.field) return unicode(kw) @dd.displayfield(_("obj2unicode")) def obj2unicode(cls, obj, ar): return dd.obj2unicode(obj)
class StartedSummaryDescription(Started): """ """ class Meta: abstract = True # iCal:SUMMARY summary = models.CharField(_("Summary"), max_length=200, blank=True) description = dd.RichTextField( _("Description"), blank=True, format='plain') # format='html') def __unicode__(self): return self._meta.verbose_name + " #" + str(self.pk) def summary_row(self, ar, **kw): elems = list(super(StartedSummaryDescription, self) .summary_row(ar, **kw)) if self.summary: elems.append(': %s' % self.summary) elems += [_(" on "), dd.dtos(self.start_date)] return elems
class RecurrentEvent(mixins.BabelNamed, RecurrenceSet, EventGenerator, UserAuthored): class Meta: app_label = 'cal' verbose_name = _("Recurring event") verbose_name_plural = _("Recurring events") abstract = dd.is_abstract_model(__name__, 'RecurrentEvent') event_type = dd.ForeignKey('cal.EventType', blank=True, null=True) description = dd.RichTextField( _("Description"), blank=True, format='html') # def on_create(self,ar): # super(RecurrentEvent,self).on_create(ar) # self.event_type = settings.SITE.site_config.holiday_event_type # def __unicode__(self): # return self.summary def update_cal_rset(self): return self def update_cal_from(self, ar): return self.start_date def update_cal_event_type(self): return self.event_type def update_cal_summary(self, et, i): return str(self) def care_about_conflicts(self, we): return False
class Note(TypedPrintable, UserAuthored, Controllable, ContactRelated, mixins.ProjectRelated, Mailable): """A **note** is a dated and timed document written by its author (a user). For example a report of a meeting or a phone call, or just some observation. Notes are usually meant for internal use. .. attribute:: date .. attribute:: time .. attribute:: type .. attribute:: event_type .. attribute:: subject .. attribute:: body .. attribute:: language """ manager_roles_required = dd.login_required(OfficeStaff) class Meta: app_label = 'notes' abstract = dd.is_abstract_model(__name__, 'Note') verbose_name = _("Note") verbose_name_plural = _("Notes") date = models.DateField(verbose_name=_('Date'), default=dd.today) time = models.TimeField(blank=True, null=True, verbose_name=_("Time"), default=timezone.now) type = dd.ForeignKey('notes.NoteType', blank=True, null=True, verbose_name=_('Note Type (Content)')) event_type = dd.ForeignKey('notes.EventType', blank=True, null=True, verbose_name=_('Event Type (Form)')) subject = models.CharField(_("Subject"), max_length=200, blank=True) body = dd.RichTextField(_("Body"), blank=True, format='html') language = dd.LanguageField() def __str__(self): return u'%s #%s' % (self._meta.verbose_name, self.pk) def summary_row(self, ar, **kw): #~ s = super(Note,self).summary_row(ui,rr) s = super(Note, self).summary_row(ar) #~ s = contacts.ContactDocument.summary_row(self,ui,rr) if self.subject: s += [' ', self.subject] return s def get_mailable_type(self): return self.type def get_print_language(self): return self.language
class RecurrentEvent(mixins.BabelNamed, RecurrenceSet, EventGenerator): """A rule designed to generate a series of recurrent events. .. attribute:: name See :attr:`lino.utils.mldbc.mixins.BabelNamed.name`. .. attribute:: every_unit Inherited from :attr:`RecurrentSet.every_unit <lino_xl.lib.cal.models.RecurrentSet.every_unit>`. .. attribute:: event_type .. attribute:: description """ class Meta: app_label = 'cal' verbose_name = _("Recurrent event rule") verbose_name_plural = _("Recurrent event rules") event_type = models.ForeignKey('cal.EventType', blank=True, null=True) description = dd.RichTextField( _("Description"), blank=True, format='html') def before_auto_event_save(self, obj): if self.end_date: # and self.end_date != self.start_date: duration = self.end_date - self.start_date obj.end_date = obj.start_date + duration super(RecurrentEvent, self).before_auto_event_save(obj) # def on_create(self,ar): # super(RecurrentEvent,self).on_create(ar) # self.event_type = settings.SITE.site_config.holiday_event_type # def __unicode__(self): # return self.summary def update_cal_rset(self): return self def update_cal_from(self, ar): return self.start_date def update_cal_event_type(self): return self.event_type def update_cal_summary(self, i): return six.text_type(self) def care_about_conflicts(self, we): """Recurrent events don't care about conflicts. A holiday won't move just because some other event has been created before on that date. """ return False
class Room(mixins.BabelNamed, ContactRelated, Colored): class Meta: app_label = 'cal' abstract = dd.is_abstract_model(__name__, 'Room') verbose_name = _("Room") verbose_name_plural = _("Rooms") description = dd.RichTextField(_("Description"), blank=True)
class Entry(UserAuthored): class Meta: verbose_name = _("Entry") verbose_name_plural = _("Entries") subject = models.CharField(_("Subject"), blank=True, max_length=200) body = dd.RichTextField(_("Body"), blank=True) company = models.ForeignKey('contacts.Company')
class EventType(mixins.BabelNamed, mixins.Sequenced, MailableType): """The possible value of the :attr:`Event.type` field. Example content: .. lino2rst:: rt.show(cal.EventTypes, limit=5) .. attribute:: is_appointment Whether events of this type should be considered "appointments" (i.e. whose time and place have been agreed upon with other users or external parties). The table (:class:`EventsByDay` and :class:`MyEvents`) show only events whose type has the `is_appointment` field checked. """ templates_group = 'cal/Event' class Meta: app_label = 'cal' abstract = dd.is_abstract_model(__name__, 'EventType') verbose_name = _("Calendar Event Type") verbose_name_plural = _("Calendar Event Types") ordering = ['seqno'] description = dd.RichTextField(_("Description"), blank=True, format='html') is_appointment = models.BooleanField(_("Event is an appointment"), default=True) all_rooms = models.BooleanField(_("Locks all rooms"), default=False) locks_user = models.BooleanField( _("Locks the user"), help_text=_("Whether events of this type make the user unavailable " "for other locking events at the same time."), default=False) start_date = models.DateField(verbose_name=_("Start date"), blank=True, null=True) event_label = dd.BabelCharField(_("Event label"), max_length=200, blank=True) # , default=_("Calendar entry")) # default values for a Babelfield don't work as expected max_conflicting = models.PositiveIntegerField( _("Simultaneous events"), help_text=_("How many conflicting events should be tolerated."), default=1) def __unicode__(self): # when selecting an Event.event_type it is more natural to # have the event_label. It seems that the current `name` field # is actually never used. return settings.SITE.babelattr(self, 'event_label') \ or settings.SITE.babelattr(self, 'name')
class Filter(dd.Model): name = models.CharField(_("Name"), max_length=200) content_type = dd.ForeignKey(contenttypes.ContentType, verbose_name=_("Model")) field_sep = models.CharField(_("Field separator"), max_length=10) help_text = dd.RichTextField(_("HelpText"), blank=True, null=True, format='plain')
class Competence(UserAuthored, Sequenced): """A **skill offer** is when a given *user* is declared to have a given *skill*. .. attribute:: user .. attribute:: end_user .. attribute:: faculty .. attribute:: affinity """ allow_cascaded_delete = "end_user user" class Meta: verbose_name = _("Skill offer") verbose_name_plural = _("Skill offers") unique_together = ['end_user', 'faculty'] faculty = dd.ForeignKey('skills.Skill') end_user = dd.ForeignKey(dd.plugins.skills.end_user_model, verbose_name=_("End user"), blank=True, null=True) # supplier = dd.ForeignKey( # dd.plugins.skills.supplier_model, # verbose_name=_("Supplier"), # blank=True, null=True) affinity = models.IntegerField( _("Affinity"), blank=True, default=MAX_WEIGHT, help_text=_("How much this user likes to get a new ticket " "in this faculty." "A number between -{0} and +{0}.").format(MAX_WEIGHT)) description = dd.RichTextField(_("Description"), blank=True) # topic = dd.ForeignKey( # 'topics.Topic', blank=True, null=True, # verbose_name=_("Option"), # help_text=_("Some skills can require additional " # "options for a competence.")) # @dd.chooser() # def topic_choices(cls, faculty): # Topic = rt.models.topics.Topic # if not faculty or not faculty.topic_group: # return Topic.objects.none() # return Topic.objects.filter(topic_group=faculty.topic_group) def full_clean(self, *args, **kw): if self.affinity is None: self.affinity = self.faculty.affinity # if self.faculty.product_cat: # if not self.product: # raise ValidationError( # "A {0} competence needs a {1} as option") super(Competence, self).full_clean(*args, **kw)
class Change(dd.Model): """A registered change in the database. Each database change of a watched object will generate one Change record. .. attribute:: master The database object which acts as "master". .. attribute:: object The database object which has been modified. """ class Meta(object): verbose_name = _("Change") verbose_name_plural = _("Changes") # allow_cascaded_delete = 'master' quick_search_fields = 'changed_fields diff' show_in_site_search = False time = models.DateTimeField() type = ChangeTypes.field() if settings.SITE.user_model: user = dd.ForeignKey(settings.SITE.user_model) else: user = dd.DummyField() object_type = dd.ForeignKey('contenttypes.ContentType', blank=True, null=True, verbose_name=_("Object type"), related_name='changes_by_object') object_id = GenericForeignKeyIdField(object_type, blank=True, null=True) object = GenericForeignKey('object_type', 'object_id', _("Object")) master_type = dd.ForeignKey('contenttypes.ContentType', blank=True, null=True, verbose_name=_("Master type"), related_name='changes_by_master') master_id = GenericForeignKeyIdField(master_type, blank=True, null=True) master = GenericForeignKey('master_type', 'master_id', _("Master")) diff = dd.RichTextField(_("Changes"), format='plain', blank=True, editable=False) changed_fields = dd.CharField(_("Fields"), max_length=250, blank=True) def __str__(self): # ~ return "#%s - %s" % (self.id,self.time) return "#%s" % self.id
class Entry(UserAuthored): class Meta: verbose_name = _("Entry") verbose_name_plural = _("Entries") date = models.DateField(_("Date")) entry_type = dd.ForeignKey(EntryType) subject = models.CharField(_("Subject"), blank=True, max_length=200) body = dd.RichTextField(_("Body"), blank=True) company = dd.ForeignKey(Company)
class Entry(mixins.CreatedModified, UserAuthored): workflow_state_field = 'state' class Meta: verbose_name = _("Entry") verbose_name_plural = _("Entries") subject = models.CharField(_("Subject"), blank=True, max_length=200) body = dd.RichTextField(_("Body"), blank=True) company = models.ForeignKey('contacts.Company', blank=True, null=True) state = EntryStates.field(blank=True, default=EntryStates.todo)
class ChatGroup(UserAuthored, Created, Referrable): class Meta(object): app_label = 'chat' verbose_name = _("Chat group") verbose_name_plural = _("Chat groups") ordering = ['created', 'id'] title = dd.CharField(max_length=20) description = dd.RichTextField(max_length=200, blank=True, null=True) ticket = dd.ForeignKey("tickets.Ticket", blank=True, null=True) @dd.action(_("getChatGroups")) def getChatGroups(self, ar): """ Returns info on all GroupChats for this user. """ qs = ChatGroupMember.objects.filter( user=ar.get_user()).select_related("group") rows = [{ "id": cp.group.pk, "title": cp.group.title, "unseen": cp.group.get_unseen_count(ar) } for cp in qs] return ar.success(rows=rows) def get_unseen_count(self, ar): """ Returns count of messages that haven't been seen yet.""" return ChatProps.objects.filter(chat__group=self, user=ar.get_user(), seen__isnull=True).count() @dd.action(_("Load GroupChat")) def loadGroupChat(self, ar): """Returns chat messages for a given chat""" rows = [] if 'mk' in ar.rqdata: # master = rt.models.resolve("contenttypes.ContentType").get(pk=ar.rqdata['mt']).get(pk=ar.rqdata["mk"]) ar.selected_rows = [ ChatGroup.objects.get(ticket__pk=ar.rqdata['mk']) ] for group in ar.selected_rows: last_ten = ChatProps.objects.filter( user=ar.get_user(), chat__group=group).order_by('-created').select_related("chat") rows.append({ 'title': group.title, 'id': group.id, 'messages': [cp.serialize(ar) for cp in last_ten] }) return ar.success(rows=rows)
class HelpText(dd.Model): """A custom help text to be displayed for a given field.""" class Meta: verbose_name = _("Help Text") verbose_name_plural = _("Help Texts") content_type = models.ForeignKey('contenttypes.ContentType', verbose_name=_("Model")) field = models.CharField(_("Field"), max_length=200) help_text = dd.RichTextField(_("HelpText"), blank=True, null=True, format='plain') def __unicode__(self): return self.content_type.app_label + '.' \ + self.content_type.model + '.' + self.field @dd.chooser(simple_values=True) def field_choices(cls, content_type): l = [] if content_type is not None: model = content_type.model_class() meta = model._meta for f in meta.fields: if not getattr(f, '_lino_babel_field', False): l.append(f.name) for f in meta.many_to_many: l.append(f.name) for f in meta.virtual_fields: l.append(f.name) for a in model.get_default_table().get_actions(): l.append(a.action.action_name) l.sort() return l #~ def get_field_display(cls,fld): #~ return fld @dd.virtualfield(models.CharField(_("Verbose name"), max_length=200)) def verbose_name(self, request): m = self.content_type.model_class() de = m.get_default_table().get_data_elem(self.field) if isinstance(de, models.Field): return "%s (%s)" % (unicode( de.verbose_name), unicode(_("database field"))) if isinstance(de, dd.VirtualField): return unicode(de.return_type.verbose_name) if isinstance(de, dd.Action): return unicode(de.label) return str(de)
class Calendar(mixins.BabelNamed): COLOR_CHOICES = [i + 1 for i in range(32)] class Meta: app_label = 'cal' abstract = dd.is_abstract_model(__name__, 'Calendar') verbose_name = _("Calendar") verbose_name_plural = _("Calendars") description = dd.RichTextField(_("Description"), blank=True, format='html') color = models.IntegerField( _("color"), default=default_color, validators=[MinValueValidator(1), MaxValueValidator(32)] )
class Entry(mixins.TypedPrintable, mixins.CreatedModified, UserAuthored, Controllable): """ Deserves more documentation. """ class Meta: verbose_name = _("Blog Entry") verbose_name_plural = _("Blog Entries") language = dd.LanguageField() type = models.ForeignKey(EntryType, blank=True, null=True) # ,null=True) title = models.CharField(_("Heading"), max_length=200, blank=True) body = dd.RichTextField(_("Body"), blank=True, format='html') def __unicode__(self): return u'%s #%s' % (self._meta.verbose_name, self.pk)
class ProductDocItem(ledger.VoucherItem, vat.QtyVatItemBase): product = dd.ForeignKey('products.Product', blank=True, null=True) #~ title = models.CharField(max_length=200,blank=True) description = dd.RichTextField(_("Description"), blank=True, null=True) discount = models.IntegerField(_("Discount"), default=0) def get_base_account(self, tt): ref = tt.get_product_base_account(self.product) return rt.models.ledger.Account.get_by_ref(ref) def product_changed(self, ar): if self.product: self.title = self.product.name self.description = self.product.description if self.qty is None: self.qty = Decimal("1") if self.product.price is not None: self.unit_price = myround(self.product.price * \ (HUNDRED - self.discount) / HUNDRED) self.unit_price_changed(ar)
class Interest(Controllable): class Meta: app_label = 'topics' verbose_name = _("Interest") verbose_name_plural = _('Interests') allow_cascaded_delete = ["partner"] topic = dd.ForeignKey('topics.Topic', related_name='interests_by_topic') remark = dd.RichTextField(_("Remark"), blank=True, format="plain") # def __str__(self): # return str(self.topic) # used in lino_tera partner = dd.ForeignKey(dd.plugins.topics.partner_model, related_name='interests_by_partner', blank=True, null=True)
class EventType(mixins.BabelNamed, Referrable, mixins.Sequenced, MailableType): templates_group = 'cal/Event' ref_max_length = 4 class Meta: app_label = 'cal' abstract = dd.is_abstract_model(__name__, 'EventType') verbose_name = _("Calendar entry type") verbose_name_plural = _("Calendar entry types") ordering = ['seqno'] description = dd.RichTextField(_("Description"), blank=True, format='html') is_appointment = models.BooleanField(_("Appointment"), default=True) all_rooms = models.BooleanField(_("Locks all rooms"), default=False) locks_user = models.BooleanField(_("Locks the user"), default=False) force_guest_states = models.BooleanField(_("Automatic presences"), default=False) fill_presences = models.BooleanField(_("Fill guests"), default=False) start_date = models.DateField(verbose_name=_("Start date"), blank=True, null=True) event_label = dd.BabelCharField(_("Entry label"), max_length=200, blank=True) # , default=_("Calendar entry")) # default values for a Babelfield don't work as expected max_conflicting = models.PositiveIntegerField(_("Simultaneous entries"), default=1) max_days = models.PositiveIntegerField(_("Maximum days"), default=1) transparent = models.BooleanField(_("Transparent"), default=False) planner_column = PlannerColumns.field(blank=True) # default_duration = models.TimeField( # _("Default duration"), blank=True, null=True) default_duration = dd.DurationField(_("Default duration"), blank=True, null=True)
class Skill(BabelNamed, Hierarchical, Sequenced): """A **skill** is a knowledge or ability which can be required in order to work e.g. on some ticket, and which individual users can have (offer) or not. """ class Meta: verbose_name = _("Skill") verbose_name_plural = _("Skills") ordering = ['name'] affinity = models.IntegerField( _("Affinity"), blank=True, default=MAX_WEIGHT, help_text=_("How much workers enjoy to get a new ticket " "requiring this skill." "A number between -{0} and +{0}.").format(MAX_WEIGHT)) skill_type = dd.ForeignKey('skills.SkillType', null=True, blank=True) remarks = dd.RichTextField(_("Remarks"), blank=True)
class MarkVoteRated(VoteAction): """Rate this vote and mark it as rated. .. attribute:: rating How you rate this job. .. attribute:: comment Your comment related to your rating. """ label = _("Rate") managed_by_votable_author = True required_states = 'assigned done invited' required_votable_states = 'new talk opened started ready' parameters = dict(rating=Ratings.field(), comment=dd.RichTextField(_("Comment"), blank=True)) # params_layout = dd.ParamsLayout(""" params_layout = dd.Panel(""" rating comment """, window_size=(50, 12)) # def param_defaults(self, obj, ar, **kw): # kw.update(rating=obj.rating) # return kw def before_execute(self, ar, obj): pv = ar.action_param_values # print(20170116, pv) obj.rating = pv.rating if pv.comment: create_row(rt.models.comments.Comment, owner=obj.votable, short_text=pv.comment, user=ar.get_user())
class Message(UserAuthored, Controllable, Created): """A **Notification message** is a instant message sent by the application to a given user. Applications can either use it indirectly by sublassing :class:`ChangeObservable <lino.modlib.notify.mixins.ChangeObservable>` or by directly calling the class method :meth:`create_message` to create a new message. .. attribute:: subject .. attribute:: body .. attribute:: user The recipient. .. attribute:: owner The database object which controls this message. This may be `None`, which means that the message has no controller. When a notification is controlled, then the recipient will receive only the first message for that object. Any following message is ignored until the recipient has "confirmed" the first message. Typical use case are the messages emitted by :class:`ChangeObservable`: you don't want to get 10 mails just because a colleague makes 10 small modifications when authoring the text field of a ChangeObservable object. .. attribute:: created .. attribute:: sent .. attribute:: seen """ class Meta(object): app_label = 'notify' verbose_name = _("Notification message") verbose_name_plural = _("Notification messages") message_type = MessageTypes.field() seen = models.DateTimeField(_("seen"), null=True, editable=False) sent = models.DateTimeField(_("sent"), null=True, editable=False) body = dd.RichTextField(_("Body"), editable=False, format='html') mail_mode = MailModes.field(default=MailModes.often.as_callable) subject = models.CharField(_("Subject"), max_length=250, editable=False) def __str__(self): return "{} #{}".format(self.message_type, self.id) # return _("About {0}").format(self.owner) # return self.message # return _("Notify {0} about change on {1}").format( # self.user, self.owner) @classmethod def emit_message(cls, ar, owner, message_type, msg_func, recipients): """Create one database object for every recipient. `recipients` is a list of `(user, mail_mode)` tuples. `msg_func` is a callable expected to return a tuple (subject, body). It is called for each recipient (in the recipient's language). The changing user does not get notified about their own changes, except when working as another user. """ # dd.logger.info("20160717 %s emit_messages()", self) others = set() me = ar.get_user() for user, mm in recipients: if user: if user != me or me.notify_myself: others.add((user, mm)) if len(others): # subject = "{} by {}".format(message_type, me) # dd.logger.info( # "Notify %s users about %s", len(others), subject) for user, mm in others: with dd.translation.override(user.language): subject_body = msg_func(user, mm) if subject_body is not None: subject, body = subject_body cls.create_message(user, owner, body=body, subject=subject, mail_mode=mm, message_type=message_type) @classmethod def create_message(cls, user, owner=None, **kwargs): """Create a message unless that user has already been notified about that object. """ if owner is not None: fltkw = gfk2lookup(cls.owner, owner) qs = cls.objects.filter(user=user, seen__isnull=True, **fltkw) if qs.exists(): return obj = cls(user=user, owner=owner, **kwargs) obj.full_clean() obj.save() if settings.SITE.use_websockets: obj.send_browser_message(user) # @dd.displayfield(_("Subject")) # def subject_more(self, ar): # if ar is None: # return '' # elems = [self.subject] # if self.body: # elems.append(' ') # # elems.append(ar.obj2html(self, _("(more)"))) # elems.append(E.raw(self.body)) # # print 20160908, elems # return E.p(*elems) # @dd.displayfield(_("Overview")) # def overview(self, ar): # if ar is None: # return '' # return self.get_overview(ar) # def get_overview(self, ar): # """Return the content to be displayed in the :attr:`overview` field. # On interactive rendererers (extjs, bootstrap3) the `obj` and # `user` are clickable. # This is also used from the :xfile:`notify/body.eml` template # where they should just be surrounded by **double asterisks** # so that Thunderbird displays them bold. # """ # elems = body_subject_to_elems(ar, self.subject, self.body) # return E.div(*elems) # # context = dict( # # obj=ar.obj2str(self.owner), # # user=ar.obj2str(self.user)) # # return _(self.message).format(**context) # # return E.p( # # ar.obj2html(self.owner), " ", # # _("was modified by {0}").format(self.user)) def unused_send_individual_email(self): """""" if not self.user.email: # debug level because we don't want to see this message # every 10 seconds: dd.logger.debug("User %s has no email address", self.user) return # dd.logger.info("20151116 %s %s", ar.bound_action, ar.actor) # ar = ar.spawn_request(renderer=dd.plugins.bootstrap3.renderer) # sar = BaseRequest( # # user=self.user, renderer=dd.plugins.bootstrap3.renderer) # user=self.user, renderer=settings.SITE.kernel.text_renderer) # tpl = dd.plugins.notify.email_subject_template # subject = tpl.format(obj=self) if self.owner is None: subject = str(self) else: subject = pgettext("notification", "{} in {}").format(self.message_type, self.owner) subject = settings.EMAIL_SUBJECT_PREFIX + subject # template = rt.get_template('notify/body.eml') # context = dict(obj=self, E=E, rt=rt, ar=sar) # body = template.render(**context) template = rt.get_template('notify/individual.eml') context = dict(obj=self, E=E, rt=rt) body = template.render(**context) sender = settings.SERVER_EMAIL rt.send_email(subject, sender, body, [self.user.email]) self.sent = timezone.now() self.save() # for testing, set show_in_workflow to True: # @dd.action(label=_("Send e-mail"), # show_in_bbar=False, show_in_workflow=False, # button_text="✉") # u"\u2709" # def do_send_email(self, ar): # self.send_individual_email() # @dd.action(label=_("Seen"), # show_in_bbar=False, show_in_workflow=True, # button_text="✓") # u"\u2713" # def mark_seen(self, ar): # self.seen = timezone.now() # self.save() # ar.success(refresh_all=True) mark_all_seen = MarkAllSeen() mark_seen = MarkSeen() clear_seen = ClearSeen() @classmethod def send_summary_emails(cls, mm): """Send summary emails for all pending notifications with the given mail_mode `mm`. """ qs = cls.objects.filter(sent__isnull=True) qs = qs.exclude(user__email='') qs = qs.filter(mail_mode=mm).order_by('user') if qs.count() == 0: return from lino.core.renderer import MailRenderer ar = rt.login(renderer=MailRenderer()) context = ar.get_printable_context() sender = settings.SERVER_EMAIL template = rt.get_template('notify/summary.eml') users = dict() for obj in qs: lst = users.setdefault(obj.user, []) lst.append(obj) dd.logger.debug("Send out %s summaries for %d users.", mm, len(users)) for user, messages in users.items(): with translation.override(user.language): if len(messages) == 1: subject = messages[0].subject else: subject = _("{} notifications").format(len(messages)) subject = settings.EMAIL_SUBJECT_PREFIX + subject context.update(user=user, messages=messages) body = template.render(**context) # dd.logger.debug("20170112 %s", body) rt.send_email(subject, sender, body, [user.email]) for msg in messages: msg.sent = timezone.now() msg.save() def send_browser_message_for_all_users(self, user): """ Send_message to all connected users """ message = { "id": self.id, "subject": self.subject, "body": html2text(self.body), "created": self.created.strftime("%a %d %b %Y %H:%M"), } # Encode and send that message to the whole channels Group for our # liveblog. Note how you can send to a channel or Group from any part # of Django, not just inside a consumer. from channels import Group Group(PUBLIC_GROUP).send({ # WebSocket text frame, with JSON content "text": json.dumps(message), }) return def send_browser_message(self, user): """ Send_message to the user's browser """ message = { "id": self.id, "subject": str(self.subject), "body": html2text(self.body), "created": self.created.strftime("%a %d %b %Y %H:%M"), } # Encode and send that message to the whole channels Group for our # Websocket. Note how you can send to a channel or Group from any part # of Django, not just inside a consumer. from channels import Group Group(user.username).send({ # WebSocket text frame, with JSON content "text": json.dumps(message), }) return
class Deployment(Sequenced, Workable): class Meta: app_label = 'deploy' verbose_name = _("Wish") verbose_name_plural = _('Wishes') # SpawnTicket = SpawnTicketFromWish(_("New Ticket"), LinkTypes.triggers) allow_cascaded_copy = 'milestone' ticket = dd.ForeignKey('tickets.Ticket', related_name="deployments_by_ticket") milestone = dd.ForeignKey(dd.plugins.tickets.milestone_model, related_name="wishes_by_milestone") remark = dd.RichTextField(_("Remark"), blank=True, format="plain") # remark = models.CharField(_("Remark"), blank=True, max_length=250) wish_type = WishTypes.field(blank=True, null=True) old_ticket_state = TicketStates.field(blank=True, null=True, verbose_name=_("Ticket State")) new_ticket_state = TicketStates.field(blank=True, null=True, verbose_name=_("New Ticket State")) deferred_to = dd.ForeignKey(dd.plugins.tickets.milestone_model, verbose_name=_("Deferred to"), blank=True, null=True, related_name="wishes_by_deferred") def get_ticket(self): return self.ticket def get_siblings(self): "Overrides :meth:`lino.mixins.Sequenced.get_siblings`" qs = self.__class__.objects.filter(milestone=self.milestone) # print(20170321, qs) return qs @dd.chooser() def unused_milestone_choices(cls, ticket): # if not ticket: # return [] # if ticket.site: # return ticket.site.milestones_by_site.all() qs = rt.models.deploy.Milestone.objects.filter(closed=False) qs = qs.order_by('label') return qs def __str__(self): return "{}@{}".format(self.seqno, self.milestone) def full_clean(self): super(Deployment, self).full_clean() if self.deferred_to: if self.milestone == self.deferred_to: raise Warning(_("Cannot defer to myself")) qs = rt.models.deploy.Deployment.objects.filter( milestone=self.deferred_to, ticket=self.ticket) if qs.count() == 0: create_row(Deployment, milestone=self.deferred_to, ticket=self.ticket, wish_type=self.wish_type, remark=self.remark) def milestone_changed(self, ar): self.ticket_changed(ar) def ticket_changed(self, ar): if self.ticket is not None and self.milestone is not None: self.milestone.add_child_stars(self.milestone, self.ticket) def after_ui_create(self, ar): # print "Create" self.ticket_changed(ar) super(Deployment, self).after_ui_create(ar) # def after_ui_save(self, ar, cw): # """ # Automatically invite every participant to vote on every wish when adding deployment. # """ # super(Deployment, self).after_ui_save(ar, cw) # self.milestone.after_ui_save(ar, cw) @dd.displayfield(_("Actions")) def workflow_buttons(self, ar, **kwargs): if ar is None: return '' l = super(Deployment, self).get_workflow_buttons(ar) sar = rt.models.comments.CommentsByRFC.insert_action.request_from(ar) owner = ContentType.objects.get(app_label='tickets', model="ticket") # sar.bound_action.icon_name = None # sar.bound_action.label = _(" New Comment") sar.known_values.update( owner_id=self.ticket.id, owner_type=owner, # owner=self.ticket, user=ar.get_user()) if sar.get_permission(): l.append(E.span(u", ")) l.append(sar.ar2button(icon_name=None, label=_("New Comment"))) # print self.E.tostring(l) return l
class Client(contacts.Person, BeIdCardHolder, UserAuthored, ClientBase, BiographyOwner, Referrable, Dupable, Lockable, Commentable, EventGenerator, Enrollable, TrendObservable): class Meta: app_label = 'avanti' verbose_name = _("Client") verbose_name_plural = _("Clients") abstract = dd.is_abstract_model(__name__, 'Client') #~ ordering = ['last_name','first_name'] quick_search_fields = "name phone gsm ref" is_obsolete = False # coachings checker beid_readonly_fields = set() manager_roles_required = dd.login_required(ClientsUser) validate_national_id = True _cef_levels = None _mother_tongues = None # in_belgium_since = models.DateField( # _("Lives in Belgium since"), blank=True, null=True) in_belgium_since = dd.IncompleteDateField(_("Lives in Belgium since"), blank=True, null=True) in_region_since = dd.IncompleteDateField(_("Lives in region since"), blank=True, null=True) starting_reason = StartingReasons.field(blank=True) old_ending_reason = OldEndingReasons.field(blank=True) ending_reason = dd.ForeignKey('avanti.EndingReason', blank=True, null=True) professional_state = ProfessionalStates.field(blank=True) category = dd.ForeignKey('avanti.Category', blank=True, null=True) translator_type = TranslatorTypes.field(blank=True) translator_notes = dd.RichTextField(_("Translator"), blank=True, format='plain') # translator = dd.ForeignKey( # "avanti.Translator", # blank=True, null=True) unemployed_since = models.DateField( _("Unemployed since"), blank=True, null=True, help_text=_("Since when the client has not been employed " "in any regular job.")) seeking_since = models.DateField( _("Seeking work since"), blank=True, null=True, help_text=_("Since when the client is seeking for a job.")) needs_work_permit = models.BooleanField(_("Needs work permit"), default=False) work_permit_suspended_until = models.DateField( blank=True, null=True, verbose_name=_("suspended until")) has_contact_pcsw = models.BooleanField(_("Has contact to PCSW"), default=False) has_contact_work_office = models.BooleanField( _("Has contact to work office"), default=False) declared_name = models.BooleanField(_("Declared name"), default=False) # is_seeking = models.BooleanField(_("is seeking work"), default=False) # removed in chatelet, maybe soon also in Eupen (replaced by seeking_since) unavailable_until = models.DateField(blank=True, null=True, verbose_name=_("Unavailable until")) unavailable_why = models.CharField(_("Reason"), max_length=100, blank=True) family_notes = models.TextField(_("Family situation"), blank=True, null=True) residence_notes = models.TextField(_("Residential situation"), blank=True, null=True) health_notes = models.TextField(_("Health situation"), blank=True, null=True) financial_notes = models.TextField(_("Financial situation"), blank=True, null=True) integration_notes = models.TextField(_("Integration notes"), blank=True, null=True) availability = models.TextField(_("Availability"), blank=True, null=True) needed_course = dd.ForeignKey('courses.Line', verbose_name=_("Needed course"), blank=True, null=True) # obstacles = models.TextField( # _("Other obstacles"), blank=True, null=True) # skills = models.TextField( # _("Other skills"), blank=True, null=True) # client_state = ClientStates.field( # default=ClientStates.newcomer.as_callable) event_policy = dd.ForeignKey('cal.EventPolicy', blank=True, null=True) language_notes = dd.RichTextField(_("Language notes"), blank=True, format='plain') remarks = dd.RichTextField(_("Remarks"), blank=True, format='plain') reason_of_stay = models.CharField(_("Reason of stay"), max_length=200, blank=True) nationality2 = dd.ForeignKey('countries.Country', blank=True, null=True, related_name='by_nationality2', verbose_name=format_lazy( u"{}{}", _("Nationality"), " (2)")) def __str__(self): info = str(self.pk) u = self.user if u is not None: # info = (str(u.initials or u) + " " + info).strip() info += "/" + str(u.initials or u.username) return "%s %s (%s)" % (self.last_name.upper(), self.first_name, info) def get_choices_text(self, ar, actor, field): if ar: u = ar.subst_user or ar.user if u.user_type.has_required_roles([ClientsNameUser]): return str(self) # 20180209 : not even the first name # return _("{} ({}) from {}").format( # self.first_name, self.pk, self.city) return _("({}) from {}").format(self.pk, self.city) # return "{} {}".format(self._meta.verbose_name, self.pk) def get_overview_elems(self, ar): elems = super(Client, self).get_overview_elems(ar) # elems.append(E.br()) elems.append(ar.get_data_value(self, 'eid_info')) notes = [] for obj in rt.models.cal.Task.objects.filter( project=self, state=TaskStates.important): notes.append(E.b(ar.obj2html(obj, obj.summary))) if len(notes): notes = join_elems(notes, " / ") elems.append(E.p(*notes, **{'class': "lino-info-yellow"})) return elems def update_owned_instance(self, owned): owned.project = self super(Client, self).update_owned_instance(owned) def full_clean(self, *args, **kw): prefix = "IP" num_width = 4 if self.ref and self.ref.upper().startswith(prefix): num_root = self.ref[len(prefix):].strip() if len(num_root) == num_width: ref_num = num_root else: qs = self.__class__.objects.filter( ref__startswith="{} {}".format(prefix, num_root)).order_by( "ref") qs = qs.exclude(id=self.id) obj = qs.last() if obj is None: last_ref = num_root.ljust(num_width, "0") else: last_ref = obj.ref[len(prefix):].strip() ref_num = str(int(last_ref) + 1) self.ref = "{} {}".format(prefix, ref_num) # if self.national_id: # ssin.ssin_validator(self.national_id) super(Client, self).full_clean(*args, **kw) def properties_list(self, *prop_ids): """Yields a list of the :class:`PersonProperty <lino_welfare.modlib.cv.models.PersonProperty>` properties of this person in the specified order. If this person has no entry for a requested :class:`Property`, it is simply skipped. Used in :xfile:`cv.odt`. ` """ return rt.models.cv.properties_list(self, *prop_ids) def get_events_user(self): return self.get_primary_coach() def update_cal_rset(self): return self.event_policy def update_cal_event_type(self): if self.event_policy is not None: return self.event_policy.event_type def update_cal_from(self, ar): return dd.today() # pc = self.get_primary_coaching() # if pc: # return pc.start_date def update_cal_until(self): return dd.today(365) # pc = self.get_primary_coaching() # if pc: # return pc.end_date def get_dupable_words(self, s): s = strip_name_prefix(s) return super(Client, self).get_dupable_words(s) def find_similar_instances(self, limit=None, **kwargs): """Overrides :meth:`lino.modlib.dupable.mixins.Dupable.find_similar_instances`, adding some additional rules. """ # kwargs.update(is_obsolete=False, national_id__isnull=True) qs = super(Client, self).find_similar_instances(None, **kwargs) if self.national_id: qs = qs.filter(national_id__isnull=True) # else: # qs = qs.filter(national_id__isnull=False) if self.birth_date: qs = qs.filter(Q(birth_date='') | Q(birth_date=self.birth_date)) last_name_words = set(self.get_dupable_words(self.last_name)) found = 0 for other in qs: found += 1 if limit is not None and found > limit: return ok = False for w in other.get_dupable_words(other.last_name): if w in last_name_words: ok = True break if ok: yield other