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::Notifications 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

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:

# 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 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:

# 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:

{
  "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:

# 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.