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
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 theaffected_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