decidim-assemblies 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/app/commands/decidim/assemblies/admin/import_assembly.rb +72 -0
  3. data/app/commands/decidim/assemblies/admin/update_assembly.rb +10 -5
  4. data/app/controllers/decidim/assemblies/admin/assembly_exports_controller.rb +24 -0
  5. data/app/controllers/decidim/assemblies/admin/assembly_imports_controller.rb +31 -0
  6. data/app/controllers/decidim/assemblies/admin/assembly_user_roles_controller.rb +2 -1
  7. data/app/controllers/decidim/assemblies/{assembly_widgets_controller.rb → widgets_controller.rb} +2 -2
  8. data/app/forms/decidim/assemblies/admin/assembly_form.rb +5 -6
  9. data/app/forms/decidim/assemblies/admin/assembly_import_form.rb +83 -0
  10. data/app/forms/decidim/assemblies/admin/assembly_user_role_form.rb +8 -2
  11. data/app/models/decidim/assemblies_type.rb +3 -0
  12. data/app/models/decidim/assembly.rb +14 -1
  13. data/app/permissions/decidim/assemblies/permissions.rb +5 -1
  14. data/app/presenters/decidim/assemblies/assembly_presenter.rb +29 -0
  15. data/app/serializers/decidim/assemblies/assembly_importer.rb +172 -0
  16. data/app/serializers/decidim/assemblies/assembly_serializer.rb +147 -0
  17. data/app/views/decidim/assemblies/admin/assemblies/index.html.erb +10 -0
  18. data/app/views/decidim/assemblies/admin/assembly_imports/_form.html.erb +46 -0
  19. data/app/views/decidim/assemblies/admin/assembly_imports/new.html.erb +7 -0
  20. data/app/views/decidim/assemblies/assemblies/show.html.erb +1 -1
  21. data/app/views/layouts/decidim/assembly.html.erb +1 -0
  22. data/config/locales/am-ET.yml +1 -0
  23. data/config/locales/bg.yml +7 -0
  24. data/config/locales/ca.yml +24 -2
  25. data/config/locales/cs.yml +29 -7
  26. data/config/locales/da.yml +1 -0
  27. data/config/locales/de.yml +19 -0
  28. data/config/locales/el.yml +19 -0
  29. data/config/locales/en.yml +22 -0
  30. data/config/locales/eo.yml +1 -0
  31. data/config/locales/es-MX.yml +22 -0
  32. data/config/locales/es-PY.yml +22 -0
  33. data/config/locales/es.yml +24 -2
  34. data/config/locales/et.yml +1 -0
  35. data/config/locales/fi-plain.yml +19 -0
  36. data/config/locales/fi.yml +22 -3
  37. data/config/locales/fr-CA.yml +22 -0
  38. data/config/locales/fr.yml +22 -0
  39. data/config/locales/hr.yml +1 -0
  40. data/config/locales/hu.yml +18 -0
  41. data/config/locales/is.yml +263 -0
  42. data/config/locales/it.yml +19 -0
  43. data/config/locales/ja-JP.yml +145 -126
  44. data/config/locales/ja.yml +471 -0
  45. data/config/locales/ko-KR.yml +1 -0
  46. data/config/locales/ko.yml +1 -0
  47. data/config/locales/lt.yml +1 -0
  48. data/config/locales/{lv-LV.yml → lv.yml} +0 -0
  49. data/config/locales/mt.yml +1 -0
  50. data/config/locales/nl.yml +19 -0
  51. data/config/locales/no.yml +19 -0
  52. data/config/locales/om-ET.yml +1 -0
  53. data/config/locales/pl.yml +37 -18
  54. data/config/locales/pt.yml +19 -0
  55. data/config/locales/ro-RO.yml +19 -0
  56. data/config/locales/sl.yml +151 -0
  57. data/config/locales/so-SO.yml +1 -0
  58. data/config/locales/sv.yml +22 -0
  59. data/config/locales/ti-ER.yml +1 -0
  60. data/config/locales/tr-TR.yml +19 -0
  61. data/config/locales/vi-VN.yml +1 -0
  62. data/config/locales/vi.yml +1 -0
  63. data/config/locales/zh-CN.yml +471 -0
  64. data/config/locales/zh-TW.yml +1 -0
  65. data/lib/decidim/assemblies/admin_engine.rb +7 -0
  66. data/lib/decidim/assemblies/engine.rb +1 -1
  67. data/lib/decidim/assemblies/participatory_space.rb +20 -12
  68. data/lib/decidim/assemblies/test/factories.rb +3 -3
  69. data/lib/decidim/assemblies/version.rb +1 -1
  70. metadata +40 -12
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Assemblies
5
+ # A factory class to ensure we always create Assemblies the same way since it involves some logic.
6
+ class AssemblyImporter < Decidim::Importers::Importer
7
+ def initialize(organization, user)
8
+ @organization = organization
9
+ @user = user
10
+ end
11
+
12
+ # Public: Creates a new Assembly.
13
+ #
14
+ # attributes - The Hash of attributes to create the Assembly with.
15
+ # user - The user that performs the action.
16
+ # opts - The options MUST contain:
17
+ # - title: The +title+ for the new Assembly
18
+ # - slug: The +slug+ for the new Assembly
19
+ #
20
+ # Returns a Assembly.
21
+ def import(attributes, _user, opts)
22
+ title = opts[:title]
23
+ slug = opts[:slug]
24
+ Decidim.traceability.perform_action!(:create, Assembly, @user, visibility: "all") do
25
+ @imported_assembly = Assembly.new(
26
+ organization: @organization,
27
+ title: title,
28
+ slug: slug,
29
+ hashtag: attributes["hashtag"],
30
+ subtitle: attributes["subtitle"],
31
+ short_description: attributes["short_description"],
32
+ description: attributes["description"],
33
+ promoted: attributes["promoted"],
34
+ developer_group: attributes["developer_group"],
35
+ local_area: attributes["local_area"],
36
+ target: attributes["target"],
37
+ participatory_scope: attributes["participatory_scope"],
38
+ participatory_structure: attributes["participatory_structure"],
39
+ show_statistics: attributes["show_statistics"],
40
+ scopes_enabled: attributes["scopes_enabled"],
41
+ private_space: attributes["private_space"],
42
+ reference: attributes["reference"],
43
+ purpose_of_action: attributes["purpose_of_action"],
44
+ composition: attributes["composition"],
45
+ duration: attributes["duration"],
46
+ creation_date: attributes["creation_date"],
47
+ decidim_scope_id: attributes["decidim_scope_id"],
48
+ closing_date_reason: attributes["closing_date_reason"],
49
+ included_at: attributes["included_at"],
50
+ closing_date: attributes["closing_date"],
51
+ created_by_other: attributes["created_by_other"],
52
+ internal_organisation: attributes["internal_organisation"],
53
+ is_transparent: attributes["is_transparent"],
54
+ special_features: attributes["special_features"],
55
+ twitter_handler: attributes["twitter_handler"],
56
+ instagram_handler: attributes["instagram_handler"],
57
+ facebook_handler: attributes["facebook_handler"],
58
+ youtube_handler: attributes["youtube_handler"],
59
+ github_handler: attributes["github_handler"],
60
+ created_by: attributes["created_by"],
61
+ meta_scope: attributes["meta_scope"]
62
+ )
63
+ @imported_assembly.remote_hero_image_url = attributes["remote_hero_image_url"] if remote_file_exists?(attributes["remote_hero_image_url"])
64
+ @imported_assembly.remote_banner_image_url = attributes["remote_banner_image_url"] if remote_file_exists?(attributes["remote_banner_image_url"])
65
+ @imported_assembly.save!
66
+ @imported_assembly
67
+ end
68
+ end
69
+
70
+ def import_assemblies_type(type_id)
71
+ return if Decidim::AssembliesType.find_by(id: type_id).nil?
72
+
73
+ @imported_assembly.decidim_assemblies_type_id = type_id
74
+ end
75
+
76
+ def import_categories(categories)
77
+ return if categories.nil?
78
+
79
+ categories.map do |category_attributes|
80
+ category = Decidim.traceability.create!(
81
+ Category,
82
+ @user,
83
+ name: category_attributes["name"],
84
+ description: category_attributes["description"],
85
+ parent_id: category_attributes["parent_id"],
86
+ participatory_space: @imported_assembly
87
+ )
88
+ next if category_attributes["subcategories"].nil?
89
+
90
+ category_attributes["subcategories"].map do |subcategory_attributes|
91
+ Decidim.traceability.create!(
92
+ Category,
93
+ @user,
94
+ name: subcategory_attributes["name"],
95
+ description: subcategory_attributes["description"],
96
+ parent_id: category.id,
97
+ participatory_space: @imported_assembly
98
+ )
99
+ end
100
+ end
101
+ end
102
+
103
+ def import_folders_and_attachments(attachments)
104
+ return if attachments["files"].nil?
105
+
106
+ attachments["files"].map do |file|
107
+ next unless remote_file_exists?(file["remote_file_url"])
108
+
109
+ file_tmp = URI.open(file["remote_file_url"])
110
+
111
+ Decidim.traceability.perform_action!("create", Attachment, @user) do
112
+ attachment = Attachment.new(
113
+ title: file["title"],
114
+ description: file["description"],
115
+ content_type: file_tmp.content_type,
116
+ attached_to: @imported_assembly,
117
+ weight: file["weight"],
118
+ file: form.file, # Define attached_to before this
119
+ file_size: file_tmp.size
120
+ )
121
+ attachment.create_attachment_collection(file["attachment_collection"])
122
+ attachment.save!
123
+ attachment
124
+ end
125
+ end
126
+
127
+ attachments["attachment_collections"].map do |collection|
128
+ Decidim.traceability.perform_action!("create", AttachmentCollection, @user) do
129
+ create_attachment_collection(collection)
130
+ end
131
+ end
132
+ end
133
+
134
+ # +components+: An Array of Hashes, each corresponding with the settings of a Decidim::Component.
135
+ def import_components(components)
136
+ return if components.nil?
137
+
138
+ importer = Decidim::Importers::ParticipatorySpaceComponentsImporter.new(@imported_assembly)
139
+ importer.import(components, @user)
140
+ end
141
+
142
+ private
143
+
144
+ def create_attachment_collection(attributes)
145
+ return unless attributes.compact.any?
146
+
147
+ attachment_collection = AttachmentCollection.find_or_initialize_by(
148
+ name: attributes["name"],
149
+ weight: attributes["weight"],
150
+ description: attributes["description"],
151
+ collection_for: @imported_assembly
152
+ )
153
+ attachment_collection.save!
154
+ attachment_collection
155
+ end
156
+
157
+ def remote_file_exists?(url)
158
+ return if url.nil?
159
+
160
+ accepted = ["image", "application/pdf"]
161
+ url = URI.parse(url)
162
+ http_connection = Net::HTTP.new(url.host, url.port)
163
+ http_connection.use_ssl = true if url.scheme == "https"
164
+ http_connection.start do |http|
165
+ return http.head(url.request_uri)["Content-Type"].start_with?(*accepted)
166
+ end
167
+ rescue StandardError
168
+ nil
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Assemblies
5
+ # This class serializes an Assembly so it can be exported to CSV, JSON or other formats.
6
+ class AssemblySerializer < Decidim::Exporters::Serializer
7
+ include Decidim::ApplicationHelper
8
+ include Decidim::ResourceHelper
9
+ include Decidim::TranslationsHelper
10
+
11
+ # Public: Initializes the serializer with a assembly.
12
+ def initialize(assembly)
13
+ @assembly = assembly
14
+ end
15
+
16
+ # Public: Exports a hash with the serialized data for this assembly.
17
+ def serialize
18
+ {
19
+ id: assembly.id,
20
+ slug: assembly.slug,
21
+ hashtag: assembly.hashtag,
22
+ decidim_organization_id: assembly.decidim_organization_id,
23
+ title: assembly.title,
24
+ subtitle: assembly.subtitle,
25
+ short_description: assembly.short_description,
26
+ description: assembly.description,
27
+ remote_hero_image_url: Decidim::Assemblies::AssemblyPresenter.new(assembly).hero_image_url,
28
+ remote_banner_image_url: Decidim::Assemblies::AssemblyPresenter.new(assembly).banner_image_url,
29
+ promoted: assembly.promoted,
30
+ developer_group: assembly.developer_group,
31
+ meta_scope: assembly.meta_scope,
32
+ local_area: assembly.local_area,
33
+ target: assembly.target,
34
+ decidim_scope_id: assembly.decidim_scope_id,
35
+ paticipatory_scope: assembly.participatory_scope,
36
+ participatory_structure: assembly.participatory_structure,
37
+ show_statistics: assembly.show_statistics,
38
+ scopes_enabled: assembly.scopes_enabled,
39
+ private_space: assembly.private_space,
40
+ reference: assembly.reference,
41
+ purpose_of_action: assembly.purpose_of_action,
42
+ composition: assembly.composition,
43
+ duration: assembly.duration,
44
+ participatory_scope: assembly.participatory_scope,
45
+ included_at: assembly.included_at,
46
+ closing_date: assembly.closing_date,
47
+ created_by: assembly.created_by,
48
+ creation_date: assembly.creation_date,
49
+ closing_date_reason: assembly.closing_date_reason,
50
+ internal_organisation: assembly.internal_organisation,
51
+ is_transparent: assembly.is_transparent,
52
+ special_features: assembly.special_features,
53
+ twitter_handler: assembly.twitter_handler,
54
+ instagram_handler: assembly.instagram_handler,
55
+ facebook_handler: assembly.facebook_handler,
56
+ youtube_handler: assembly.youtube_handler,
57
+ github_handler: assembly.github_handler,
58
+ created_by_other: assembly.created_by_other,
59
+ decidim_assemblies_type_id: assembly.decidim_assemblies_type_id,
60
+ area: {
61
+ id: assembly.area.try(:id),
62
+ name: assembly.area.try(:name) || empty_translatable
63
+ },
64
+ scope: {
65
+ id: assembly.scope.try(:id),
66
+ name: assembly.scope.try(:name) || empty_translatable
67
+ },
68
+
69
+ assembly_categories: serialize_categories,
70
+ attachments: {
71
+ attachment_collections: serialize_attachment_collections,
72
+ files: serialize_attachments
73
+ },
74
+ components: serialize_components
75
+ }
76
+ end
77
+
78
+ private
79
+
80
+ attr_reader :assembly
81
+
82
+ def serialize_categories
83
+ return unless assembly.categories.first_class.any?
84
+
85
+ assembly.categories.first_class.map do |category|
86
+ {
87
+ id: category.try(:id),
88
+ name: category.try(:name),
89
+ description: category.try(:description),
90
+ parent_id: category.try(:parent_id),
91
+ subcategories: serialize_subcategories(category.subcategories)
92
+ }
93
+ end
94
+ end
95
+
96
+ def serialize_subcategories(subcategories)
97
+ return unless subcategories.any?
98
+
99
+ subcategories.map do |subcategory|
100
+ {
101
+ id: subcategory.try(:id),
102
+ name: subcategory.try(:name),
103
+ description: subcategory.try(:description),
104
+ parent_id: subcategory.try(:parent_id)
105
+ }
106
+ end
107
+ end
108
+
109
+ def serialize_attachment_collections
110
+ return unless assembly.attachment_collections.any?
111
+
112
+ assembly.attachment_collections.map do |collection|
113
+ {
114
+ id: collection.try(:id),
115
+ name: collection.try(:name),
116
+ weight: collection.try(:weight),
117
+ description: collection.try(:description)
118
+ }
119
+ end
120
+ end
121
+
122
+ def serialize_attachments
123
+ return unless assembly.attachments.any?
124
+
125
+ assembly.attachments.map do |attachment|
126
+ {
127
+ id: attachment.try(:id),
128
+ title: attachment.try(:title),
129
+ weight: attachment.try(:weight),
130
+ description: attachment.try(:description),
131
+ attachment_collection: {
132
+ name: attachment.attachment_collection.try(:name),
133
+ weight: attachment.attachment_collection.try(:weight),
134
+ description: attachment.attachment_collection.try(:description)
135
+ },
136
+ remote_file_url: Decidim::AttachmentPresenter.new(attachment).attachment_file_url
137
+ }
138
+ end
139
+ end
140
+
141
+ def serialize_components
142
+ serializer = Decidim::Exporters::ParticipatorySpaceComponentsSerializer.new(@assembly)
143
+ serializer.serialize
144
+ end
145
+ end
146
+ end
147
+ end
@@ -8,6 +8,12 @@
8
8
 
9
9
  <%= t("assemblies", scope: "decidim.admin.titles") %>
10
10
 
11
+ <% if allowed_to? :import, :assembly %>
12
+ <%= link_to t("actions.import_assembly", scope: "decidim.admin"),
13
+ new_import_path,
14
+ class: "button tiny button--title" %>
15
+ <% end %>
16
+
11
17
  <% if allowed_to? :create, :assembly %>
12
18
  <%= link_to t("actions.new_assembly", scope: "decidim.admin"),
13
19
  new_assembly_path(parent_id: parent_assembly&.id),
@@ -76,6 +82,10 @@
76
82
  <% end %>
77
83
  </td>
78
84
  <td class="table-list__actions">
85
+ <% if allowed_to? :create, :assembly, assembly: assembly %>
86
+ <%= icon_link_to "data-transfer-download", assembly_export_path(assembly), t("actions.export", scope: "decidim.admin"), method: :post, class: "action-icon--export" %>
87
+ <% end %>
88
+
79
89
  <% if allowed_to? :create, :assembly, assembly: assembly %>
80
90
  <%= icon_link_to "clipboard", new_assembly_copy_path(assembly), t("actions.duplicate", scope: "decidim.admin"), class: "action-icon--copy" %>
81
91
  <% end %>
@@ -0,0 +1,46 @@
1
+ <%= javascript_include_tag "decidim/slug_form" %>
2
+
3
+ <div class="card" id="assemblies">
4
+ <div class="card-divider">
5
+ <h2 class="card-title"><%= title %></h2>
6
+ </div>
7
+ <div class="card-section">
8
+ <div class="row column">
9
+ <div class="row">
10
+ <div class="column xlarge-8">
11
+ <%= form.translated :text_field, :title, autofocus: true %>
12
+ </div>
13
+ <div class="column xlarge-4 slug">
14
+ <%= form.text_field :slug %>
15
+ <p class="help-text"><%== t(".slug_help", url: decidim_form_slug_url(:assemblies, form.object.slug)) %></p>
16
+ </div>
17
+ <div class="column xlarge-8">
18
+ <fieldset>
19
+ <legend><%= t(".document_legend") %> </legend>
20
+ <div class="row column">
21
+ <%= form.upload :document, optional: false %>
22
+ </div>
23
+ </fieldset>
24
+ </div>
25
+ </div>
26
+ <div class="card">
27
+ <div class="card-divider">
28
+ <legend><%= select %></legend>
29
+ </div>
30
+ <div class="card-section">
31
+ <div class="row">
32
+ <div class="columns xlarge-3">
33
+ <%= form.check_box :import_categories %>
34
+ </div>
35
+ <div class="columns xlarge-3">
36
+ <%= form.check_box :import_attachments %>
37
+ </div>
38
+ <div class="columns xlarge-6">
39
+ <%= form.check_box :import_components %>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
@@ -0,0 +1,7 @@
1
+ <%= decidim_form_for(@form, url: imports_path, method: :post, html: { class: "form import_assembly" }) do |f| %>
2
+ <%= render partial: "form", object: f, locals: { title: t("assembly_imports.new.title", scope: "decidim.admin"), select: t("assembly_imports.new.select", scope: "decidim.admin") } %>
3
+
4
+ <div class="button--double form-general-submit">
5
+ <%= f.submit t("assembly_imports.new.import", scope: "decidim.admin") %>
6
+ </div>
7
+ <% end %>
@@ -209,7 +209,7 @@ edit_link(
209
209
  </div>
210
210
  <%= resource_reference(current_participatory_space) %>
211
211
  <%= render partial: "decidim/shared/share_modal" %>
212
- <%= embed_modal_for assembly_assembly_widget_url(current_participatory_space, format: :js) %>
212
+ <%= embed_modal_for assembly_widget_url(current_participatory_space, format: :js) %>
213
213
  </div>
214
214
  </div>
215
215
 
@@ -7,6 +7,7 @@
7
7
 
8
8
  <%= render "layouts/decidim/application" do %>
9
9
  <%= render partial: "layouts/decidim/assembly_header" %>
10
+ <%= cell "decidim/translation_bar", current_organization %>
10
11
  <div class="wrapper">
11
12
  <%= yield %>
12
13
  </div>
@@ -0,0 +1 @@
1
+ am:
@@ -0,0 +1,7 @@
1
+ bg:
2
+ activemodel:
3
+ attributes:
4
+ assembly:
5
+ area_id: Район
6
+ assembly_type: Тип събрание
7
+ assembly_type_other: Друг тип събрание
@@ -64,6 +64,12 @@ ca:
64
64
  email: Correu electrònic
65
65
  name: Nom
66
66
  role: Rol de participant
67
+ errors:
68
+ models:
69
+ assembly:
70
+ attributes:
71
+ document:
72
+ invalid_document_type: 'Tipus de document invàlid. Els formats que s''accepten són: %{valid_mime_types}'
67
73
  activerecord:
68
74
  models:
69
75
  decidim/assembly:
@@ -78,6 +84,7 @@ ca:
78
84
  decidim:
79
85
  admin:
80
86
  actions:
87
+ import_assembly: Importar
81
88
  new_assembly: Nova assemblea
82
89
  new_assembly_type: Nou tipus d'assemblea
83
90
  assemblies:
@@ -122,6 +129,14 @@ ca:
122
129
  copy: Còpia
123
130
  select: Selecciona quines dades vols duplicar
124
131
  title: Assemblea duplicada
132
+ assembly_imports:
133
+ create:
134
+ error: Hi ha hagut un problema important aquesta assemblea.
135
+ success: Assemblea importada amb èxit.
136
+ new:
137
+ import: Importar
138
+ select: Selecciona quines dades vols importar
139
+ title: Importar assemblea
125
140
  assembly_members:
126
141
  create:
127
142
  error: S'ha produït un error en afegir un membre per a aquesta assemblea.
@@ -269,6 +284,10 @@ ca:
269
284
  assembly_copies:
270
285
  form:
271
286
  slug_help: 'Els noms curts d''URL s''utilitzen per generar els URL que apunten a aquesta assemblea. Només accepta lletres, números i guions, i ha de començar amb una lletra. Exemple: %{url}'
287
+ assembly_imports:
288
+ form:
289
+ document_legend: Afegir un document
290
+ slug_help: 'Els noms curts d''URL s''utilitzen per generar les URL que apunten a aquesta assemblea. Només accepta lletres, números i guions, i ha de començar amb una lletra. Exemple: %{url}'
272
291
  assembly_members:
273
292
  form:
274
293
  existing_user: Participant existent
@@ -286,6 +305,9 @@ ca:
286
305
  content_blocks:
287
306
  highlighted_assemblies:
288
307
  max_results: Quantitat màxima d'elements que es mostraran
308
+ new_import:
309
+ accepted_types:
310
+ json: JSON
289
311
  assembly_members:
290
312
  index:
291
313
  members: Membres
@@ -385,10 +407,10 @@ ca:
385
407
  events:
386
408
  assemblies:
387
409
  create_assembly_member:
388
- email_intro: Una administradora de la assemblea <a href="%{resource_url}">%{resource_name}</a> t'ha afegit com a un dels seus membres.
410
+ email_intro: Una administradora de l'assemblea <a href="%{resource_url}">%{resource_name}</a> t'ha afegit com a un dels seus membres.
389
411
  email_outro: Reps aquesta notificació perquè t'han convidat a una assemblea. Entra en <a href="%{resource_path}">assembly page</a> per a contribuir-hi!
390
412
  email_subject: T'han convidat a ser membre de l'assemblea %{resource_name}!
391
- notification_title: T'han registrat com a membre de l'assemblea <a href="%{resource_path}">%{resource_name}</a>. Entra a <a href="%{resource_path}">assembly page</a> per a contribuir-hi!
413
+ notification_title: T'han registrat com a membre de l'assemblea <a href="%{resource_path}">%{resource_name}</a>. Entra a <a href="%{resource_path}">la pàgina de l'assemblea</a> per a contribuir-hi!
392
414
  assembly:
393
415
  role_assigned:
394
416
  email_intro: T'han assignat el rol de %{role} a l'assemblea "%{resource_title}".