.. doctest docs/specs/notify.rst .. _book.specs.notify: ================================== ``notify``: Notification framework ================================== .. currentmodule:: lino.modlib.notify The :mod:`lino.modlib.notify` plugin adds a notification framework to your Lino application. .. contents:: :depth: 1 :local: .. include:: /../docs/shared/include/tested.rst >>> import lino >>> lino.startup('lino_book.projects.chatter.settings.demo') >>> from lino.api.shell import * >>> from pprint import pprint Overview ======== A **notification message** is a message sent by the application to a user of the site. Lino sends notifications as `Desktop notifications`_ and/or by email depending on the user preferences of the recipient. In addition, notification messages are sent via email to the user according to his :attr:`mail_mode` field. Don't mix up notifications with "system notes" (implemented by :mod:`lino_xl.lib.notes`). These are fundamentally different beasts which partly resemble each other. For example both types of messages have a "subject" and a "body". Both describe some event that happens to some database object. But while *notifications* are messages to be quickly sent to their recipients, *system notes* are permanent historic entries visible to every user (who has the required permission). The emitter of a notification message is currently not stored. That is, you cannot currently request to see a list of all messages emitted by a given user. Notification messages are emitted by the application code - Either manually by calling the class method :meth:`Message.create_message` to create a new message. - Have your models inherit from :class:`ChangeNotifier`. - Add actions that inherit from :class:`NotifyingAction`. Desktop notifications ===================== To enable desktop notifications, there are some requirements: - :attr:`lino.core.site.Site.use_websockets` must be `True` - notifications must be properly installed on the server - the user must have their browser open and have signed in to the Lino site - the user must give their browser permission to show desktop notifications from the Lino site Marking notifications as seen ============================= doctest docs/specs/notify.rst In addition to sending notifications via email and as desktop notification, Lino displays unseen notfication messages in the dashboard where it also provides an action for marking individual message as seen. A common caveat is that Lino does not know whether you saw the desktop notification or the email. That's why all notifications remain on your dashboard until you tick them off explicitly. - It can be disturbing to read a message again in the dasboard if you have just read by email or as a desktop notification. - Some users tend to not care about marking their notifications as seen in the dashboard, which causes their "My notification messages" to become overfilled and useless. - Some users misunderstand the my notifications widget as a to-do list, which is not a good idea. There is no perfect solution for these problems. One workaround is to instruct Lino to also delete *unseen* notifications automatically, by setting :attr:`keep_unseen ` to `False`. Here is an example which also increases :attr:`remove_after ` to 240 hours (10 days):: SITE.plugins.notify.configure(remove_after=240, keep_unseen=False) Users can hide the `MyMessages` widget in their preferences if :mod:`lino.modlib.dashboard` is installed as well. But that's not a recommended solution. If you see that users of your application are doing this, you should analyze why they do it and e.g. add filtering options. Possible optimizations of the system: - Marking notifications as seen in the dashboard can be a bit slow because Lino refreshes the whole dashboard after every click. We could avoid this using javascript which sets the item to hidden instead of calling refresh. - Add a ¬ify=123456" (the id of the message) to every link in the email so that when the user follows one of them, the message can get marked as seen. Notification messages ===================== .. class:: Message The Django model that represents a *notification 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:`ChangeNotifier`: you don't want to get 10 mails just because a colleague makes 10 small modifications when authoring the text field of a ChangeNotifier object. .. attribute:: created .. attribute:: sent .. attribute:: seen .. method:: emit_notification(cls, ar, owner, message_type, msg_func, recipients) Class method which creates one database object per recipient. `recipients` is an iterable of `(user, mail_mode)` tuples. Duplicate items, items with user being `None` and items having :attr:`mail_mode ` set to :attr:`silent ` are removed. `msg_func` is a callable expected to return a tuple `(subject, body)`. It is called for each recipient in the recipient's language. The emitting user does not get notified, except when working as another user or when notify_myself is set. .. method:: create_message(cls, user, owner=None, **kwargs) Create a message unless that user has already been notified about that object. .. method:: send_summary_emails(cls, mm) Send summary emails for all pending notifications with the given mail_mode `mm`. .. method:: send_browser_message_for_all_users(self, user) Send_message to all connected users .. method:: send_browser_message(self, user) Send_message to the user's browser .. class:: Messages Base for all tables of messages. .. class:: AllMessages(Messages) The gobal list of all messages. .. class:: MyMessages(Messages) Shows messages emitted to me. As an :term:`application developer` you should understand the different meanings of "subject" and "body": - The body is expected to be a self-sufficient and complete description of the event. If a message has a *body*, then the *subject* is **not** being displayed in the MyMessages summary. - The subject might contain limited rich text (text formatting, links) but be aware that this formatting gets lost when the message is sent as an email. Change notifiers ================ .. class:: ChangeNotifier .. class:: ChangeNotifier Mixin for models which can emit notifications to a list of "observers" when an instance is modified. TODO: rename ChangeNotifier to ChangeNotifier .. method:: get_change_subject(self, ar, cw) Returns the subject text of the notification message to emit. The default implementation returns a message of style "{user} modified|created {object}" . Returning None or an empty string means to suppress notification. .. method:: add_change_watcher(self, user) Parameters: :user: The user that will be linked to this object as a change watcher. .. method:: get_change_body(self, ar, cw) Returns the body text of the notification message to emit. The default implementation returns a message of style "{object} has been modified by {user}" followed by a summary of the changes. .. method:: get_change_info(self, ar, cw) Return a list of HTML elements to be inserted into the body. This is called by :meth:`get_change_body`. Subclasses can override this. Usage example :class:`lino_xl.lib.notes.models.Note` .. method:: get_change_owner(self) Return the owner of the notification to emit. The "owner" is "the database object we are talking about" and decides who is observing this object. Notifying actions ================= .. class:: NotifyingAction An action which pops up a dialog window of three fields "Summary", "Description" and a checkbox "Don't notify others" to optionally suppress notification. Screenshot of a notifying action: .. image:: /images/screenshots/reception.CheckinVisitor.png :scale: 50 Dialog fields: .. attribute:: notify_subject .. attribute:: notify_body .. attribute:: notify_silent .. method:: get_notify_subject(self, ar, obj) Return the default value of the `notify_subject` field. .. method:: get_notify_body(self, ar, obj) Return the default value of the `notify_body` field. .. method:: get_notify_owner(self, ar, obj) Expected to return the :attr:`owner lino.modlib.notify.Message.owner>` of the message. The default returns `None`. `ar` is the action request, `obj` the object on which the action is running, .. method:: get_notify_recipients(self, ar, obj) Yield a list of users to be notified. `ar` is the action request, `obj` the object on which the action is running, A :class:`NotifyingAction` is a dialog action which potentially sends a notification. It has three dialog fields ("subject", "body" and a checkbox "silent"). You can have non-dialog actions (or actions with some other dialog than a simple subject and body) which build a custom subject and body and emit a notification. If the emitting object also has a method :meth:`emit_system_note`, then this is being called as well. Local configuration =================== .. setting:: CHANNEL_LAYERS This plugin inspects some process parameters in order to automagically set a default value for the :setting:`CHANNEL_LAYERS` setting. >>> from django.conf import settings >>> from lino.core.utils import is_devserver >>> is_devserver() True >>> settings.SITE.use_websockets True >>> settings.CHANNEL_LAYERS['default']['BACKEND'] in ['asgiref.inmemory.ChannelLayer','channels_redis.core.RedisChannelLayer'] True >>> settings.CHANNEL_LAYERS['default'].get('ROUTING','') in ['lino.modlib.notify.routing.channel_routing',''] True How to configure locally on a production site:: SITE = Site(...) CHANNEL_LAYERS['default']['BACKEND'] = 'asgi_redis.RedisChannelLayer' CHANNEL_LAYERS['default']['CONFIG'] = { 'hosts': [('localhost', 6379)], } Utility functions ================= .. function:: send_pending_emails_often() .. function:: send_pending_emails_daily() .. function:: clear_seen_messages Daily task which deletes messages older than :attr:`remove_after` hours. Choicelists =========== .. class:: MessageTypes The list of possible choices for the `message_type` field of a :class:`Message`. .. class:: MailModes How the system should send email notifications to a user. .. attribute:: silent Disable notifications for this user. .. attribute:: never Notify in Lino but never send email. Actions ======= .. class:: MarkSeen Mark this message as seen. .. class:: MarkAllSeen Mark all messages as seen. .. class:: ClearSeen Mark this message as not yet seen. Templates used by this plugin ============================= .. xfile:: notify/body.eml A Jinja template used for generating the body of the email when sending a message per email to its recipient. Available context variables: - ``obj`` -- The :class:`Message` instance being sent. - ``E`` -- The html namespace :mod:`etgen.html` - ``rt`` -- The runtime API :mod:`lino.api.rt` - ``ar`` -- The action request which caused the message. a :class:`BaseRequest ` instance.