decidim 0.20.0 → 0.23.1.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of decidim might be problematic. Click here for more details.

@@ -0,0 +1,238 @@
1
+ # How to fix metrics
2
+
3
+ At the request of some instances, we have analyzed the issues related to metrics and looked for possible solutions.
4
+
5
+ ## Problems
6
+
7
+ We have identified two main problems:
8
+
9
+ - Metrics generation crashing, which cause `MetricJob`s to run again and again.
10
+ - Peaks in generated metrics, sudden changes from day to day when displaying metrics.
11
+
12
+ ### Metrics generation crashing
13
+
14
+ We have identified only one culprit here: "orphans" records, meaning records whose related component or participatory space cannot be found in the database. This is because in a previous decidim release `PartipatorySpaces` could be deleted but they were not deleted properly. So any application that has deleted a participatory space in the past, will probably have unrelated records that will make some metrics calculation crash.
15
+
16
+ ### Peaks in generated metrics
17
+
18
+ If somehow the metrics jobs fail to execute for a period of time, big differences can appear in metrics. So first make sure that you have metrics for every day, if not [generate them](https://github.com/decidim/decidim/blob/master/docs/advanced/metrics.md).
19
+
20
+ If you have metrics generated for almost everyday and still see drastic changes from day to day, take into account that changing the visibility of a component or participatory space (making them private or unpublishing them) will naturally cause big differences in generated metrics.
21
+
22
+ Finally, if you see that the differences in some days are multiples of a previous generated metric, meaning suddenly you have exactly the double or the triple of a calculated metric, it's very likely that you have duplicate generated metrics. We have only seen this problem with instances using Sidekiq, not Delayed Job. We do not know the cause of this, but it seems to be a known issue [Avoiding duplicate jobs in Sidekiq](https://blog.francium.tech/avoiding-duplicate-jobs-in-sidekiq-dcbb1aca1e20).
23
+
24
+ ## Solutions
25
+
26
+ We cannot offer a definitive solution for duplicate metrics, other than to delete old duplicate metrics and generate them again. If this problem persists, however, consider using Delayed Job.
27
+ For a given metric type (`rake decidim:metrics:list`) that has duplicates:
28
+
29
+ - Option 1: Remove individually each metric record per day.
30
+ - Option 2: Delete all metric records and recalculate them. [CHANGELOG](https://github.com/decidim/decidim/blob/release/0.18-stable/CHANGELOG.md#participants-metrics) of decidim version 0.18 has an example for "participants".
31
+
32
+ For orphan records, you can do the following:
33
+
34
+ - Back up the database.
35
+ - Delete orphan records fromt the console (code is below).
36
+ - Delete "comments" metrics and recalculate them following the [aforementioned example](https://github.com/decidim/decidim/blob/release/0.18-stable/CHANGELOG.md#participants-metrics).
37
+
38
+ ### Some queries that may help
39
+
40
+ ```ruby
41
+ GROUP_BY_FIELDS= %w(
42
+ day
43
+ metric_type
44
+ decidim_organization_id
45
+ participatory_space_type
46
+ participatory_space_id
47
+ related_object_type
48
+ related_object_id
49
+ decidim_category_id).join(', ')
50
+
51
+ def remove_duplicates
52
+ sql= <<~EOSQL.strip
53
+ DELETE FROM decidim_metrics WHERE decidim_metrics.id NOT IN
54
+ (SELECT id FROM (
55
+ SELECT DISTINCT ON (#{GROUP_BY_FIELDS}) * FROM decidim_metrics));
56
+ EOSQL
57
+ end
58
+
59
+ # DELETE FROM decidim_metrics WHERE decidim_metrics.id NOT IN \n (SELECT id FROM (\n SELECT DISTINCT ON (day, metric_type, decidim_organization_id, participatory_space_type, participatory_space_id, related_object_type, related_object_id, decidim_category_id) * FROM decidim_metrics));
60
+ def count_duplicates
61
+ sql= <<~EOSQL.strip
62
+ SELECT count(1), #{GROUP_BY_FIELDS} FROM decidim_metrics GROUP BY #{GROUP_BY_FIELDS} HAVING COUNT(1) > 1;
63
+ EOSQL
64
+ end
65
+ ```
66
+
67
+ ### Delete orphan records
68
+
69
+ "proposals", "meetings", "accountability", "debates", "pages", "budgets", "surveys"
70
+
71
+ #### Proposals
72
+
73
+ Delete proposals whose component does not have a participatory space and delete components of a proposal type that do not have a participatory space
74
+
75
+ ```ruby
76
+ Decidim::Component.where(manifest_name: "proposals").find_each(batch_size: 100) { |c|
77
+ if c.participatory_space.blank?
78
+ Decidim::Proposals::Proposal.where(component: c).destroy_all
79
+ c.destroy
80
+ end
81
+ }
82
+ ```
83
+
84
+ Delete proposals that do not have a component
85
+
86
+ ```ruby
87
+ Decidim::Proposals::Proposal.find_each(batch_size: 100) { |proposal|
88
+ proposal.delete if proposal.component.blank?
89
+ }
90
+ ````
91
+
92
+ #### Meetings
93
+
94
+ Delete meetings whose component has no participatory space and delete components of meeting type that do not have a participatory space
95
+
96
+ ```ruby
97
+ Decidim::Component.where(manifest_name: "meetings").find_each(batch_size: 100) { |c|
98
+ if c.participatory_space.blank?
99
+ Decidim::Meetings::Meeting.where(component: c).destroy_all
100
+ c.destroy
101
+ end
102
+ }
103
+ ```
104
+
105
+ Delete meetings that do not have a component
106
+
107
+ ```ruby
108
+ Decidim::Meetings::Meeting.find_each(batch_size: 100) { |meeting|
109
+ meeting.delete if meeting.component.blank?
110
+ }
111
+ ````
112
+
113
+ #### Debates
114
+
115
+ Delete debates that its component has no participatory space and the debate components that do not have a participatory space
116
+
117
+ ```ruby
118
+ Decidim::Component.where(manifest_name: "debates").find_each(batch_size: 100) { |c|
119
+ if c.participatory_space.blank?
120
+ Decidim::Debates::Debate.where(component: c).destroy_all
121
+ c.destroy
122
+ end
123
+ }
124
+ ```
125
+
126
+ Destroy debates that do not have a component
127
+
128
+ ```ruby
129
+ Decidim::Debates::Debate.find_each(batch_size: 100) { |debate|
130
+ debate.delete if debate.component.blank?
131
+ }
132
+ ```
133
+
134
+ #### Posts
135
+
136
+ Destroy posts whose component has no participatory space and blog components that do not have a participatory space
137
+
138
+ ```ruby
139
+ Decidim::Component.where(manifest_name: "blogs").find_each(batch_size: 100) { |c|
140
+ if c.participatory_space.blank?
141
+ Decidim::Blogs::Post.where(component: c).destroy_all
142
+ c.destroy
143
+ end
144
+ }
145
+ ```
146
+
147
+ Destroy posts that do not have a component
148
+
149
+ ```ruby
150
+ Decidim::Blogs::Post.find_each(batch_size: 100) { |post|
151
+ post.delete if post.component.blank?
152
+ }
153
+ ```
154
+
155
+ #### Accountability
156
+
157
+ Destroy results whose component has no participatory space and components of accountability type that do not have a participatory space
158
+
159
+ ```ruby
160
+ Decidim::Component.where(manifest_name: "accountability").find_each(batch_size: 100) { |c|
161
+ if c.participatory_space.blank?
162
+ Decidim::Accountability::Result.where(component: c).destroy_all
163
+ c.destroy
164
+ end
165
+ }
166
+ ```
167
+
168
+ Destroy results that do not have a component
169
+
170
+ ```ruby
171
+ Decidim::Accountability::Result.find_each(batch_size: 100) { |result|
172
+ result.delete if result.component.blank?
173
+ }
174
+ ```
175
+
176
+ #### Pages
177
+
178
+ Destroy page components that do not have a participatory space
179
+
180
+ ```ruby
181
+ Decidim::Component.where(manifest_name: "pages").find_each(batch_size: 100) { |c|
182
+ if c.participatory_space.blank?
183
+ c.destroy
184
+ end
185
+ }
186
+ ```
187
+
188
+ #### Budgets
189
+
190
+ Destroy projects whose component has no participatory space and budget components that do not have a participatory space
191
+
192
+ ```ruby
193
+ Decidim::Component.where(manifest_name: "budgets").find_each(batch_size: 100) { |c|
194
+ if c.participatory_space.blank?
195
+ Decidim::Budgets::Project.where(component: c).destroy_all
196
+ c.destroy
197
+ end
198
+ }
199
+ ```
200
+
201
+ Destroy results that do not have a component
202
+
203
+ ```ruby
204
+ Decidim::Budgets::Project.find_each(batch_size: 100) { |project|
205
+ project.delete if project.component.blank?
206
+ }
207
+ ```
208
+
209
+ #### Surveys
210
+
211
+ ```ruby
212
+ Decidim::Component.where(manifest_name: "surveys").find_each(batch_size: 100) { |c|
213
+ if c.participatory_space.blank?
214
+ Decidim::Surveys::Survey.where(component: c).destroy_all
215
+ c.destroy
216
+ end
217
+ }
218
+ ```
219
+
220
+ Destroy surveys that do not have a component
221
+
222
+ ```ruby
223
+ Decidim::Surveys::Survey.find_each(batch_size: 100) { |survey|
224
+ survey.delete if survey.component.blank?
225
+ }
226
+ ```
227
+
228
+ #### Comments
229
+
230
+ Destroy comments whose commentable root is a proposal that does not have a participatory space.
231
+
232
+ ```ruby
233
+ proposal_ids = Decidim::Comments::Comment.where(decidim_root_commentable_type: "Decidim::Proposals::Proposal").pluck(:decidim_root_commentable_id)
234
+
235
+ proposal_ids_without_space = Decidim::Proposals::Proposal.where(id: proposal_ids).find_all{|p| p.participatory_space.blank? }.pluck(:id)
236
+
237
+ Decidim::Comments::Comment.where(decidim_root_commentable_type: "Decidim::Proposals::Proposal", decidim_root_commentable_id: proposal_ids_without_space).destroy_all
238
+ ```
@@ -0,0 +1,12 @@
1
+ # Create your own machine translation service
2
+
3
+ You can use the `Decidim::Dev::DummyTranslator` service as a base. Any new translator service will need to implement the same API as this class.
4
+
5
+ ## Integrating with async services
6
+
7
+ Some translation services are async, which means that some extra work is needed. This is the main overview:
8
+
9
+ - The Translation service will only send the translation request. It should have a way to send what resource, field and target locale are related to that translation.
10
+ - You'll need to create a custom controller in your application to receive the callback from the translation service when the translation is finished
11
+ - From that new endpoint, find a way to find the related resource, field and target locale. Then start a `Decidim::MachineTranslationSaveJob` with that data. This job will handle how to save the data in the DB.
12
+
@@ -37,6 +37,7 @@ Metrics calculations must be executed everyday. Some `rake task` have been added
37
37
  ```ruby
38
38
  bundle exec rake decidim:metrics:list
39
39
  ```
40
+
40
41
  Currently, available metrics are:
41
42
 
42
43
  - **users**, created `Users`
@@ -66,7 +67,7 @@ Only available for `ParticipatorySpaces` (restricted to `ParticipatoryProcesses`
66
67
 
67
68
  ## Configuration
68
69
 
69
- - A **crontab** line must be added to your server to maintain them updated daily. You could use [Whenever](https://github.com/javan/whenever) to manage it directly from the APP
70
+ - A **crontab** line must be added to your server to maintain them updated daily. You could use [Whenever](https://github.com/javan/whenever) to manage it directly from the APP. You probably want to schedule a `bundle exec rake decidim:metrics:all` every night.
70
71
  - An **ActiveJob** queue, like [Sidekiq](https://github.com/mperham/sidekiq) or [DelayedJob](https://github.com/collectiveidea/delayed_job/)
71
72
 
72
73
  ## Persistence
@@ -78,20 +79,17 @@ persist metrics from all times and types.
78
79
  The `decidim_metrics` table has the following fields:
79
80
 
80
81
  - `day`: the day for which the metric has been computed.
81
- - `metric_type`: the type of the metric. One of: users, proposals,
82
- accepted_proposals, supports, assemblies.
82
+ - `metric_type`: the type of the metric. One of: users, proposals, accepted_proposals, supports, assemblies.
83
83
  - `cumulative`: quantity accumulated to day ”day”.
84
84
  - `quantity`: quantity for the current day, ”day”.
85
- - `decidim_organization_id`: the FK to the organization to which this Metric
86
- belongs to.
87
- - `participatory_space_type` + `participatory_space_id`: the FK to the
88
- participatory space to which this Metric belongs to, if any.
89
- - `related_object_type` + `related_object_id`: the FK to the object to which
90
- this Metric belongs to, if any.
85
+ - `decidim_organization_id`: the FK to the organization to which this Metric belongs to.
86
+ - `participatory_space_type` + `participatory_space_id`: the FK to the participatory space to which this Metric belongs to, if any.
87
+ - `related_object_type` + `related_object_id`: the FK to the object to which this Metric belongs to, if any.
91
88
  - `decidim_category_id`: the FK to the category for this Metric, if any.
92
89
 
93
90
  Relations around `decidim_metrics` table:
94
- ```
91
+
92
+ ```ascii
95
93
  +------------------------+
96
94
  +--------------+ | ParticipatoryProcesses |
97
95
  | Organization | +----+------------------------+
@@ -0,0 +1,64 @@
1
+ # Newsletter templates
2
+
3
+ The newsletter templates allow the user to select a template amongst a set of of them, and use it as base to send newsletters. This allow for more customization on newsletter, tematic newsletters, etc.
4
+
5
+ Code-wise, they use the content blocks system internally, so [check the docs](https://github.com/decidim/decidim/blob/master/docs/advanced/content_blocks.md) for that section first.
6
+
7
+ ## Adding a new template
8
+
9
+ You'll first need to register the template as a content block, but specifying `:newsletter_template` as its scope:
10
+
11
+ ```ruby
12
+ Decidim.content_blocks.register(:newsletter_template, :my_template) do |content_block|
13
+ content_block.cell "decidim/newsletter_templates/my_template"
14
+ content_block.settings_form_cell = "decidim/newsletter_templates/my_template_settings_form"
15
+ content_block.public_name_key "decidim.newsletter_templates.my_template.name"
16
+
17
+ content_block.images = [
18
+ {
19
+ name: :main_image,
20
+ uploader: "Decidim::NewsletterTemplateImageUploader",
21
+ preview: -> { ActionController::Base.helpers.asset_path("decidim/placeholder.jpg") }
22
+ }
23
+ ]
24
+
25
+ content_block.settings do |settings|
26
+ settings.attribute(
27
+ :body,
28
+ type: :text,
29
+ translated: true,
30
+ preview: -> { ([I18n.t("decidim.newsletter_templates.my_template.body_preview")] * 100).join(" ") }
31
+ )
32
+ end
33
+ end
34
+ ```
35
+
36
+ You'll need to add this into an initializer. Note that if you're adding this from a module, then you need to add it from the `engine.rb` file (check the docs for content blocks for more info).
37
+
38
+ This is the simplest template. It has a single attribute, in this case a translatable chunk of text. Let's go line by line.
39
+
40
+ Inside the block, first we define the path of the cell we'll use to render the email publicly. This cell will receive the `Decidim::ContentBlock` object, which will contain the attributes and any image we have (one in this example). In order to render cells, please note that emails have a very picky HTML syntax, so we suggest using some specialized tools to design the template, export the layout to HTML and render that through the cell. We suggest you make this cell inherit from `Decidim::NewsletterTemplates::BaseCell` for convenience.
41
+
42
+ Then we define the cell that will be used to render the form in the admin section. This form needs to show inputs for the attributes defined when registering the template. It will receive the `form` object to render the input fields. We suggest this cell to inherit from `Decidim::NewsletterTemplates::BaseSettingsFormCell`.
43
+
44
+ In the third line inside the block we define the I18n path to the public name of the template. This name will serve as identifier for the users who write the newsletters, so be sure to make it descriptive.
45
+
46
+ After that we define the images this newsletter supports. We give it a unique name, the class name of the uploader we'll use (the example one is the default one, but you might want to customize this value) and a way to preview this image. This preview image will only be used in the "Preview" page of the template, it's not a fallback. If you want a fallback, please implement it through a custom uploader (see `carrierwave`'s docs for that). There's no limit of the amount of images you add.
47
+
48
+ Finally, we define the attributes for the newsletter. In this case we define a body attribute, which is a translatable text. Whether this text require an editor or not will be defined by the settings cell. We also have a way to preview that attribute. There's no limit on the number of attributes you define.
49
+
50
+ ## Interpolating the recipient name
51
+
52
+ Decidim accepts `%{name}` as a placeholder for the recipient name. If you want your template to use it, you'll need to call `parse_interpolations` in your public cell:
53
+
54
+ ```ruby
55
+ class Decidim::NewsletterTemplates::MyTemplate < Decidim::ViewModel
56
+ include Decidim::NewslettersHelper
57
+
58
+ def body
59
+ parse_interpolations(uninterpolated_body, recipient_user, newsletter.id)
60
+ end
61
+ end
62
+ ```
63
+
64
+ The newsletter subject is automatically interpolated.
@@ -0,0 +1,114 @@
1
+ # Notifications
2
+
3
+ In Decidim, notifications may mean two things:
4
+
5
+ - he concept of notifying an event to a user. This is the wider use of "notification".
6
+ - the notification's participant space, which lists the `Decidim::Notification`s she has received.
7
+
8
+ So, in the wider sense, notifications are messages that are sent to the users, admins or participants, when something interesting occurs in the platform.
9
+
10
+ Each notification is sent via two communication channels: email and internal notifications.
11
+
12
+ ## A Decidim Event
13
+
14
+ Technically, a Decidim event is nothing but an `ActiveSupport::Notification` with a payload of the form
15
+
16
+ ```ruby
17
+ ActiveSupport::Notifications.publish(
18
+ event,
19
+ event_class: event_class.name,
20
+ resource: resource,
21
+ affected_users: affected_users.uniq.compact,
22
+ followers: followers.uniq.compact,
23
+ extra: extra
24
+ )
25
+ ```
26
+
27
+ To publish an event to send a notification, Decidim's `EventManager` should be used:
28
+
29
+ ```ruby
30
+ # 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.
31
+ data = {
32
+ event: "decidim.events.comments.comment_created",
33
+ event_class: Decidim::Comments::CommentCreatedEvent,
34
+ resource: comment.root_commentable,
35
+ extra: {
36
+ comment_id: comment.id
37
+ },
38
+ affected_users: [user1, user2],
39
+ followers: [user3, user4]
40
+ }
41
+
42
+ Decidim::EventsManager.publish(data)
43
+ ```
44
+
45
+ Both, `EmailNotificationGenerator` and `NotificationGenerator` are use the same arguments:
46
+
47
+ - **event**: A String with the name of the event.
48
+ - **event_class**: A class that wraps the event.
49
+ - **resource**: an instance of a class implementing the `Decidim::Resource` concern.
50
+ - **followers**: a collection of Users that receive the notification because they're following it.
51
+ - **affected_users**: a collection of Users that receive the notification because they're affected by it.
52
+ - **force_send**: boolean indicating if EventPublisherJob should skip the `notifiable?` check it performs before notifying.
53
+ - **extra**: a Hash with extra information to be included in the notification.
54
+
55
+ Again, both generators will check for each user
56
+
57
+ - in the *followers* array, if she has the `notification_types` set to "all" or "followed-only".
58
+ - in the *affected_users* array, if she has the `notification_types` set to "all" or "own-only".
59
+
60
+ 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.
61
+
62
+ 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`.
63
+
64
+ Please refer to [Ruby on Rails Notifications documentation](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) if you need to hack the Decidim's events system.
65
+
66
+ ## How Decidim's `EventPublisherJob` processes the events?
67
+
68
+ 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.
69
+
70
+ When invoked it simply performs some validations and enqueue an `EmailNotificationGeneratorJob` and a `NotificationGeneratorJob`.
71
+
72
+ The validations it performs check if the resource, the component, or the participatory space are published (when the concept applies to the artifact).
73
+
74
+ ## The \*Event class
75
+
76
+ Generates the email and notification messages from the information related with the notification.
77
+
78
+ Event classes are subclasses of `Decidim::Events::SimpleEvent`.
79
+ A subset of the payload of the notification is passed to the event class's constructor:
80
+
81
+ - The `resource`
82
+ - The `event` name
83
+ - The notified user, either from the `followers` or from the `affected_users` arrays
84
+ - The `extra` hash, with content specific for the given SimpleEvent subclass
85
+ - The user_role, either :follower or :affected_user
86
+
87
+ With the previous information the event class is able to generate the following contents.
88
+
89
+ Developers will be able to customize those messages by adding translations to the `config/locales/en.yml` file of the corresponding module.
90
+ 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.)
91
+
92
+ ### Email contents
93
+
94
+ The following are the parts of the notification email:
95
+
96
+ - *email_subject*, to be customized
97
+ - email_greeting, with a good default, usually there's no need to cusomize it
98
+ - *email_intro*, to be customized
99
+ - *resource_text* (optional), rendered `html_safe` if present
100
+ - *resource_url*, a link to the involved resource if resource_url and resource_title are present
101
+ - *email_outro*
102
+
103
+ All contents except the `email_greeting` use to require customization on each notification.
104
+
105
+ ### Notification contents
106
+
107
+ 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.
108
+
109
+ ## Testing notifications
110
+
111
+ - Test that the event has been published (usually a command test)
112
+ - Test the event returns the expected contents for the email and the notification.
113
+
114
+ Developers should we aware when adding URLs in the email's content, be sure to use absolute URLs and not relative paths.