= Notifications In Decidim, notifications may mean two things: * he concept of notifying an event to a user. This is the wider use of "notification". * the notification's participant space, which lists the ``Decidim::Notification``s she has received. So, in the wider sense, notifications are messages that are sent to the users, admins or participants, when something interesting occurs in the platform. Each notification is sent via two communication channels: email and internal notifications. == A Decidim Event Technically, a Decidim event is nothing but an `ActiveSupport::Notification` with a payload of the form [source,ruby] ---- ActiveSupport::Notifications.publish( event, event_class: event_class.name, resource: resource, affected_users: affected_users.uniq.compact, followers: followers.uniq.compact, extra: extra ) ---- To publish an event to send a notification, Decidim's `EventManager` should be used: [source,ruby] ---- # Note the convention between the `event` key, and the `event_class` that will be used later to wrap the payload and be used as the email or notification model. data = { event: "decidim.events.comments.comment_created", event_class: Decidim::Comments::CommentCreatedEvent, resource: comment.root_commentable, extra: { comment_id: comment.id }, affected_users: [user1, user2], followers: [user3, user4] } Decidim::EventsManager.publish(**data) ---- Both, `EmailNotificationGenerator` and `NotificationGenerator` are use the same arguments: * *event*: A String with the name of the event. * *event_class*: A class that wraps the event. * *resource*: an instance of a class implementing the `Decidim::Resource` concern. * *followers*: a collection of Users that receive the notification because they are following it. * *affected_users*: a collection of Users that receive the notification because they are affected by it. * *force_send*: boolean indicating if EventPublisherJob should skip the `notifiable?` check it performs before notifying. * *extra*: a Hash with extra information to be included in the notification. Again, both generators will check for each user * in the _followers_ array, if she has the `notification_types` set to "all" or "followed-only". * in the _affected_users_ array, if she has the `notification_types` set to "all" or "own-only". Event names must start with "decidim.events." (the `event` data key). This way `Decidim::EventPublisherJob` will automatically process them. Otherwise no artifact in Decidim will process them, and will be the developer's responsibility to subscribe to them and process. Sometimes, when something that must be notified to users happen, a service is defined to manage the logic involved to decide which events should be published. See for example `Decidim::Comments::NewCommentNotificationCreator`. Please refer to https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html[Ruby on Rails Notifications documentation] if you need to hack the Decidim's events system. == How Decidim's `EventPublisherJob` processes the events? The `EventPublisherJob` in Decidim's core engine subscribes to all notifications matching the regular expression `+/^decidim\.events\./+`. This is, starting with "decidim.events.". It will then be invoked when an imaginary event named "decidim.events.harmonica_blues" is published. When invoked it simply performs some validations and enqueue an `EmailNotificationGeneratorJob` and a `NotificationGeneratorJob`. The validations it performs check if the resource, the component, or the participatory space are published (when the concept applies to the artifact). == The *Event class Generates the email and notification messages from the information related with the notification. Event classes are subclasses of `Decidim::Events::SimpleEvent`. A subset of the payload of the notification is passed to the event class's constructor: * The `resource` * The `event` name * The notified user, either from the `followers` or from the `affected_users` arrays * The `extra` hash, with content specific for the given SimpleEvent subclass * The user_role, either :follower or :affected_user With the previous information the event class is able to generate the following contents. Developers will be able to customize those messages by adding translations to the `config/locales/en.yml` file of the corresponding module. The keys to be used will have the translation scope corresponding to the event name ("decidim.events.comments.comment_by_followed_user" for example) and the key will be the content to override (email_subject, email_intro, etc.) === Email contents The following are the parts of the notification email: * _email_subject_, to be customized * email_greeting, with a good default, usually there is no need to customize it * _email_intro_, to be customized * _resource_text_ (optional), rendered `html_safe` if present * _resource_url_, a link to the involved resource if resource_url and resource_title are present * _email_outro_ All contents except the `email_greeting` use to require customization on each notification. === Notification contents Only the `notification_title` is generated in the event class. The rest of the contents are produced by the templates from the `resource` and the `notification` objects. === Notification actions It is possible to render actions into the notifications area. These actions are typically one or more buttons that the user can click to perform an action related to the notification. In order to add actions to your notification, you need to implement the methods `action_cell`, `action_data` in your event class. The `action_cell` method should return the name of the cell that will be rendered in the notification area. The `action_data` method should return the data that will be passed to the cell. Currently, there is only one action cell available, `Decidim::Notifications::Actions::ButtonCell`. This cell renders a list of buttons with the text and URL provided in the `action_data`. See the code for the `Decidim::InvitedToGroupEvent` to render actions that allow users to accept or reject a membership invitation to a group: [source,ruby] ---- # decidim-core/app/events/decidim/invited_to_group_event.rb def membership_id extra["membership_id"] end def invitation @invitation ||= UserGroupMembership.find_by(user:, id: membership_id, role: "invited") end def action_cell "decidim/notification_actions/buttons" if invitation end def action_data [ { url: url_helpers.group_invite_path(user_group_nickname, membership_id, format: :json), icon: "check-line", method: "patch", i18n_label: "decidim.group_invites.accept_invitation" }, { url: url_helpers.group_invite_path(user_group_nickname, membership_id, format: :json), icon: "close-circle-line", method: "delete", i18n_label: "decidim.group_invites.reject_invitation" } ] end ---- The previous code will render a couple of buttons to accept/reject the invitation but only if the UserGroupMembership is not accepted yet. Note that the cell returned is "decidim/notification_actions/buttons", if you want to use a custom cell, you should create it in your application and return it accordingly. The default buttons cell renders as many buttons as defined in the `action_data` array. Each button will handle a "click" event that will make an AJAX request to the URL provided in the button data. The `method` attribute is used to define the HTTP method that will be used in the AJAX request. So, it is advisable for the controller handling the request to respond with a JSON object with the following structure: [source,json] ---- { "message": "Some message" } ---- Use standard HTTP status codes to indicate the result of the operation. For an example, see the implementation of the `Decidim::GroupInvitesController` that responds with a JSON object only when requests are made with the `:json` format: [source,ruby] ---- # decidim-core/app/controllers/decidim/group_invites_controller.rb def update enforce_permission_to :accept, :user_group_invitations AcceptGroupInvitation.call(inviting_user_group, current_user) do on(:ok) do respond_to do |format| format.json do render json: { message: t("group_invites.accept.success", scope: "decidim") } end format.all do flash[:notice] = t("group_invites.accept.success", scope: "decidim") redirect_to profile_groups_path(current_user.nickname) end end end on(:invalid) do respond_to do |format| format.json do render json: { message: t("group_invites.accept.error", scope: "decidim") }, status: :unprocessable_entity end format.all do flash[:alert] = t("group_invites.accept.error", scope: "decidim") redirect_to profile_groups_path(current_user.nickname) end end end end end def destroy enforce_permission_to :reject, :user_group_invitations RejectGroupInvitation.call(inviting_user_group, current_user) do on(:ok) do respond_to do |format| format.json do render json: { message: t("group_invites.reject.success", scope: "decidim") } end format.all do flash[:notice] = t("group_invites.reject.success", scope: "decidim") redirect_to profile_groups_path(current_user.nickname) end end end on(:invalid) do respond_to do |format| format.json do render json: { message: t("group_invites.reject.error", scope: "decidim") }, status: :unprocessable_entity end format.all do flash[:alert] = t("group_invites.reject.error", scope: "decidim") redirect_to profile_groups_path(current_user.nickname) end end end end end ---- == Testing notifications * Test that the event has been published (usually a command test) * Test the event returns the expected contents for the email and the notification. Developers should we aware when adding URLs in the email's content, be sure to use absolute URLs and not relative paths.