decidim-core 0.13.1 → 0.14.1

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

Potentially problematic release.


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

Files changed (236) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/decidim_core_manifest.js +1 -1
  3. data/app/assets/images/decidim/gamification/badges/invitations.svg +117 -0
  4. data/app/assets/javascripts/decidim.js.es6 +4 -1
  5. data/app/assets/javascripts/decidim/ajax_modals.js.es6 +17 -0
  6. data/app/assets/javascripts/decidim/conferences.js.es6 +16 -0
  7. data/app/assets/javascripts/decidim/input_hashtags.js.es6 +115 -0
  8. data/app/assets/javascripts/decidim/input_mentions.js.es6 +2 -3
  9. data/app/assets/javascripts/decidim/vizzs/areachart.js.es6 +226 -0
  10. data/app/assets/javascripts/decidim/vizzs/metrics.js.es6 +26 -0
  11. data/app/assets/javascripts/decidim/vizzs/orgchart.js.es6 +701 -0
  12. data/app/assets/javascripts/decidim/vizzs/renders.js.es6 +11 -0
  13. data/app/assets/stylesheets/decidim/extras/_proposal_form.scss +3 -1
  14. data/app/assets/stylesheets/decidim/layouts/_home.scss +1 -1
  15. data/app/assets/stylesheets/decidim/modules/_areachart.scss +74 -0
  16. data/app/assets/stylesheets/decidim/modules/_badges.scss +116 -0
  17. data/app/assets/stylesheets/decidim/modules/_buttons.scss +5 -0
  18. data/app/assets/stylesheets/decidim/modules/_cards.scss +21 -4
  19. data/app/assets/stylesheets/decidim/modules/_chart-tooltip.scss +42 -0
  20. data/app/assets/stylesheets/decidim/modules/_collapsible-list.scss +12 -8
  21. data/app/assets/stylesheets/decidim/modules/_conference-nav.scss +31 -0
  22. data/app/assets/stylesheets/decidim/modules/_conference-programme.scss +110 -0
  23. data/app/assets/stylesheets/decidim/modules/_conference-speaker.scss +86 -0
  24. data/app/assets/stylesheets/decidim/modules/_conversation.scss +58 -0
  25. data/app/assets/stylesheets/decidim/modules/_help.scss +38 -0
  26. data/app/assets/stylesheets/decidim/modules/_hover-section.scss +29 -0
  27. data/app/assets/stylesheets/decidim/modules/_icons.scss +10 -4
  28. data/app/assets/stylesheets/decidim/modules/_input-hashtags.scss +124 -0
  29. data/app/assets/stylesheets/decidim/modules/_loading-spinner.scss +12 -0
  30. data/app/assets/stylesheets/decidim/modules/_margins.scss +2 -2
  31. data/app/assets/stylesheets/decidim/modules/_modules.scss +15 -0
  32. data/app/assets/stylesheets/decidim/modules/_navbar.scss +9 -0
  33. data/app/assets/stylesheets/decidim/modules/_orgchart.scss +62 -0
  34. data/app/assets/stylesheets/decidim/modules/_status-labels.scss +2 -1
  35. data/app/assets/stylesheets/decidim/modules/_typography.scss +9 -0
  36. data/app/assets/stylesheets/decidim/utils/_helpers.scss +28 -0
  37. data/app/assets/stylesheets/decidim/utils/_mixins.scss +63 -0
  38. data/app/cells/decidim/author/withdraw.erb +1 -1
  39. data/app/cells/decidim/author_cell.rb +1 -1
  40. data/app/cells/decidim/badge/show.erb +36 -0
  41. data/app/cells/decidim/badge_cell.rb +53 -0
  42. data/app/cells/decidim/badges/show.erb +6 -0
  43. data/app/cells/decidim/badges_cell.rb +14 -0
  44. data/app/cells/decidim/card_m/header.erb +1 -1
  45. data/app/cells/decidim/card_m/show.erb +1 -2
  46. data/app/cells/decidim/card_m/top.erb +7 -0
  47. data/app/cells/decidim/card_m_cell.rb +14 -17
  48. data/app/cells/decidim/coauthorships_cell.rb +77 -0
  49. data/app/cells/decidim/collapsible_authors/show.erb +0 -1
  50. data/app/cells/decidim/collapsible_authors_cell.rb +4 -4
  51. data/app/cells/decidim/collapsible_list/show.erb +12 -4
  52. data/app/cells/decidim/collapsible_list_cell.rb +14 -12
  53. data/app/cells/decidim/content_blocks/footer_sub_hero/show.erb +14 -0
  54. data/app/cells/decidim/content_blocks/footer_sub_hero_cell.rb +12 -0
  55. data/app/{views/decidim/pages/home/_hero.html.erb → cells/decidim/content_blocks/hero/show.erb} +4 -4
  56. data/app/cells/decidim/content_blocks/hero_cell.rb +25 -0
  57. data/app/cells/decidim/content_blocks/hero_settings_form/show.erb +7 -0
  58. data/app/cells/decidim/content_blocks/hero_settings_form_cell.rb +13 -0
  59. data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +24 -0
  60. data/app/cells/decidim/content_blocks/highlighted_content_banner_cell.rb +16 -0
  61. data/app/{views/decidim/pages/home/_extended.html.erb → cells/decidim/content_blocks/how_to_participate/show.erb} +10 -10
  62. data/app/cells/decidim/content_blocks/how_to_participate_cell.rb +9 -0
  63. data/app/{views/decidim/pages/home/_statistics.html.erb → cells/decidim/content_blocks/stats/show.erb} +2 -2
  64. data/app/cells/decidim/content_blocks/stats_cell.rb +18 -0
  65. data/app/{views/decidim/pages/home/_sub_hero.html.erb → cells/decidim/content_blocks/sub_hero/show.erb} +2 -2
  66. data/app/cells/decidim/content_blocks/sub_hero_cell.rb +17 -0
  67. data/app/cells/decidim/conversation/show.erb +18 -0
  68. data/app/cells/decidim/conversation_cell.rb +23 -0
  69. data/app/cells/decidim/conversation_header/show.erb +17 -0
  70. data/app/cells/decidim/conversation_header_cell.rb +16 -0
  71. data/app/cells/decidim/conversations/show.erb +45 -0
  72. data/app/cells/decidim/conversations_cell.rb +24 -0
  73. data/app/cells/decidim/follow_button/show.erb +3 -3
  74. data/app/cells/decidim/follow_button_cell.rb +1 -5
  75. data/app/cells/decidim/following_cell.rb +1 -7
  76. data/app/cells/decidim/message/show.erb +15 -0
  77. data/app/cells/decidim/message_cell.rb +23 -0
  78. data/app/cells/decidim/new_conversation/show.erb +19 -0
  79. data/app/cells/decidim/new_conversation_cell.rb +19 -0
  80. data/app/cells/decidim/notifications/show.erb +1 -1
  81. data/app/cells/decidim/profile/show.erb +27 -0
  82. data/app/cells/decidim/profile_cell.rb +33 -0
  83. data/app/cells/decidim/profile_sidebar/show.erb +57 -0
  84. data/app/cells/decidim/profile_sidebar_cell.rb +31 -0
  85. data/app/cells/decidim/tos_page_cell.rb +0 -4
  86. data/app/cells/decidim/user_profile/header.erb +1 -1
  87. data/app/controllers/concerns/decidim/action_authorization.rb +13 -38
  88. data/app/controllers/concerns/decidim/needs_permission.rb +15 -6
  89. data/app/controllers/decidim/application_controller.rb +1 -0
  90. data/app/controllers/decidim/authorization_modals_controller.rb +35 -0
  91. data/app/controllers/decidim/components/base_controller.rb +0 -1
  92. data/app/controllers/decidim/devise/invitations_controller.rb +2 -1
  93. data/app/controllers/decidim/messaging/conversations_controller.rb +2 -11
  94. data/app/controllers/decidim/newsletters_controller.rb +4 -6
  95. data/app/controllers/decidim/notifications_controller.rb +4 -0
  96. data/app/controllers/decidim/pages_controller.rb +3 -7
  97. data/app/controllers/decidim/profiles_controller.rb +17 -7
  98. data/app/forms/decidim/notifications_settings_form.rb +1 -1
  99. data/app/forms/decidim/registration_form.rb +1 -1
  100. data/app/helpers/decidim/action_authorization_helper.rb +51 -46
  101. data/app/helpers/decidim/application_helper.rb +18 -0
  102. data/app/helpers/decidim/card_helper.rb +1 -1
  103. data/app/helpers/decidim/cells_helper.rb +6 -2
  104. data/app/helpers/decidim/resource_helper.rb +8 -1
  105. data/app/helpers/decidim/searches_helper.rb +5 -4
  106. data/app/helpers/decidim/traceability_helper.rb +5 -1
  107. data/app/models/decidim/authorization.rb +2 -2
  108. data/app/models/decidim/content_block.rb +144 -0
  109. data/app/models/decidim/gamification/badge_score.rb +13 -0
  110. data/app/models/decidim/messaging/message.rb +1 -1
  111. data/app/models/decidim/messaging/receipt.rb +1 -1
  112. data/app/models/decidim/organization.rb +1 -5
  113. data/app/models/decidim/resource_permission.rb +8 -0
  114. data/app/models/decidim/searchable_resource.rb +1 -1
  115. data/app/models/decidim/user.rb +17 -1
  116. data/app/permissions/decidim/default_permissions.rb +4 -3
  117. data/app/permissions/decidim/permissions.rb +33 -1
  118. data/app/presenters/decidim/hashtag_presenter.rb +32 -0
  119. data/app/presenters/decidim/resource_locator_presenter.rb +13 -0
  120. data/app/presenters/decidim/user_presenter.rb +1 -1
  121. data/app/queries/decidim/messaging/user_conversations.rb +1 -1
  122. data/app/resolvers/decidim/hashtags_resolver.rb +15 -0
  123. data/app/services/decidim/action_authorizer.rb +9 -8
  124. data/app/types/decidim/core/date_time_type.rb +1 -1
  125. data/app/types/decidim/core/hashtag_type.rb +13 -0
  126. data/app/uploaders/decidim/homepage_image_uploader.rb +1 -1
  127. data/app/uploaders/decidim/image_uploader.rb +1 -0
  128. data/app/views/decidim/authorization_modals/show.html.erb +32 -0
  129. data/app/views/decidim/messaging/conversations/create.js.erb +1 -1
  130. data/app/views/decidim/messaging/conversations/index.html.erb +1 -51
  131. data/app/views/decidim/messaging/conversations/new.html.erb +1 -5
  132. data/app/views/decidim/messaging/conversations/show.html.erb +1 -9
  133. data/app/views/decidim/messaging/conversations/update.js.erb +1 -1
  134. data/app/views/decidim/notifications/index.html.erb +1 -0
  135. data/app/views/decidim/pages/decidim_page.html.erb +9 -0
  136. data/app/views/decidim/pages/home.html.erb +12 -16
  137. data/app/views/decidim/pages/index.html.erb +8 -0
  138. data/app/views/decidim/profiles/_user_follow.erb +2 -2
  139. data/app/views/decidim/profiles/show.html.erb +1 -37
  140. data/app/views/decidim/searches/_results.html.erb +1 -1
  141. data/app/views/decidim/shared/_author_reference.html.erb +1 -1
  142. data/app/views/decidim/shared/_authorization_modal.html.erb +1 -0
  143. data/app/views/decidim/shared/_tags.html.erb +1 -1
  144. data/app/views/kaminari/decidim/_page.html.erb +1 -1
  145. data/app/views/layouts/decidim/_application.html.erb +6 -1
  146. data/app/views/layouts/decidim/_edit_link.html.erb +8 -0
  147. data/app/views/layouts/decidim/_impersonation_warning.html.erb +1 -1
  148. data/app/views/layouts/decidim/_user_menu.html.erb +2 -2
  149. data/app/views/layouts/decidim/_wrapper.html.erb +14 -1
  150. data/config/initializers/carrierwave.rb +15 -0
  151. data/config/locales/ca.yml +78 -30
  152. data/config/locales/en.yml +78 -30
  153. data/config/locales/es-PY.yml +78 -30
  154. data/config/locales/es.yml +78 -30
  155. data/config/locales/eu.yml +78 -30
  156. data/config/locales/fi.yml +262 -214
  157. data/config/locales/fr.yml +78 -30
  158. data/config/locales/gl.yml +78 -30
  159. data/config/locales/hu.yml +781 -0
  160. data/config/locales/it.yml +78 -30
  161. data/config/locales/nl.yml +78 -30
  162. data/config/locales/pl.yml +78 -30
  163. data/config/locales/pt-BR.yml +106 -58
  164. data/config/locales/pt.yml +78 -30
  165. data/config/locales/ru.yml +52 -32
  166. data/config/locales/sv.yml +183 -135
  167. data/config/locales/uk.yml +60 -40
  168. data/config/routes.rb +8 -6
  169. data/db/migrate/20180705091019_create_decidim_resource_permissions.rb +12 -0
  170. data/db/migrate/20180706104107_add_nickname_to_managed_users.rb +14 -0
  171. data/db/migrate/20180706111847_fix_result_follows.rb +9 -0
  172. data/db/migrate/20180724103814_add_content_blocks.rb +22 -0
  173. data/db/migrate/20180726112510_create_decidim_hashtags.rb +17 -0
  174. data/db/migrate/20180730071851_add_core_content_blocks.rb +28 -0
  175. data/db/migrate/20180802132147_rename_content_block_options_to_settings.rb +7 -0
  176. data/db/migrate/20180806095628_add_badge_scores.rb +11 -0
  177. data/db/migrate/20180808135006_add_images_to_content_blocks.rb +7 -0
  178. data/db/migrate/20180810092428_move_organization_fields_to_hero_content_block.rb +23 -0
  179. data/db/seeds.rb +10 -2
  180. data/lib/decidim/api/authorable_interface.rb +1 -1
  181. data/lib/decidim/coauthorable.rb +1 -0
  182. data/lib/decidim/content_block_manifest.rb +58 -0
  183. data/lib/decidim/content_block_registry.rb +87 -0
  184. data/lib/decidim/content_parsers.rb +1 -0
  185. data/lib/decidim/content_parsers/hashtag_parser.rb +36 -0
  186. data/lib/decidim/content_processor.rb +11 -0
  187. data/lib/decidim/content_renderers.rb +1 -0
  188. data/lib/decidim/content_renderers/hashtag_renderer.rb +43 -0
  189. data/lib/decidim/core.rb +28 -6
  190. data/lib/decidim/core/api.rb +1 -0
  191. data/lib/decidim/core/engine.rb +52 -1
  192. data/lib/decidim/core/test.rb +3 -0
  193. data/lib/decidim/core/test/factories.rb +32 -17
  194. data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +10 -0
  195. data/lib/decidim/core/test/shared_examples/coauthorable.rb +3 -0
  196. data/lib/decidim/core/test/shared_examples/edit_link_shared_examples.rb +30 -0
  197. data/lib/decidim/core/test/shared_examples/has_space_in_mcell_examples.rb +15 -0
  198. data/lib/decidim/core/test/shared_examples/publicable.rb +1 -1
  199. data/lib/decidim/core/test/shared_examples/railtie_examples.rb +15 -0
  200. data/lib/decidim/core/test/shared_examples/scope_helper_examples.rb +1 -0
  201. data/lib/decidim/core/version.rb +1 -1
  202. data/lib/decidim/events/base_event.rb +2 -1
  203. data/lib/decidim/form_builder.rb +9 -3
  204. data/lib/decidim/friendly_dates.rb +1 -1
  205. data/lib/decidim/gamification.rb +109 -0
  206. data/lib/decidim/gamification/badge.rb +54 -0
  207. data/lib/decidim/gamification/badge_earned_event.rb +9 -0
  208. data/lib/decidim/gamification/badge_registry.rb +63 -0
  209. data/lib/decidim/gamification/badge_scorer.rb +118 -0
  210. data/lib/decidim/gamification/badge_status.rb +41 -0
  211. data/lib/decidim/gamification/base_event.rb +40 -0
  212. data/lib/decidim/gamification/level_up_event.rb +9 -0
  213. data/lib/decidim/hashtag.rb +15 -0
  214. data/lib/decidim/hashtaggable.rb +20 -0
  215. data/lib/decidim/query_extensions.rb +10 -0
  216. data/lib/decidim/resource_manifest.rb +10 -0
  217. data/lib/decidim/resourceable.rb +13 -0
  218. data/lib/decidim/search_resource_fields_mapper.rb +8 -3
  219. data/lib/decidim/searchable.rb +8 -0
  220. data/lib/decidim/translatable_attributes.rb +6 -18
  221. data/lib/decidim/view_model.rb +6 -0
  222. data/lib/devise/models/decidim_newsletterable.rb +1 -1
  223. data/vendor/assets/javascripts/d3.js +17813 -0
  224. metadata +125 -27
  225. data/app/cells/decidim/card_m/author.erb +0 -3
  226. data/app/cells/decidim/card_m/authors.erb +0 -9
  227. data/app/views/decidim/messaging/conversations/_message.html.erb +0 -14
  228. data/app/views/decidim/messaging/conversations/_reply.html.erb +0 -11
  229. data/app/views/decidim/messaging/conversations/_show.html.erb +0 -21
  230. data/app/views/decidim/messaging/conversations/_start.html.erb +0 -12
  231. data/app/views/decidim/pages/home/_footer_sub_hero.html.erb +0 -14
  232. data/app/views/decidim/pages/home/_highlighted_content_banner.html.erb +0 -26
  233. data/app/views/decidim/pages/home/_highlighted_processes.html.erb +0 -7
  234. data/app/views/decidim/profiles/_user.html.erb +0 -59
  235. data/app/views/decidim/shared/_action_authorization_modal.html.erb +0 -39
  236. data/app/views/layouts/decidim/_component_authorization_modals.html.erb +0 -5
@@ -4,6 +4,16 @@ require "spec_helper"
4
4
 
5
5
  shared_examples_for "authorable interface" do
6
6
  describe "author" do
7
+ describe "when author is not present" do
8
+ before do
9
+ model.update(author: author)
10
+ end
11
+
12
+ it "does not include the author" do
13
+ expect(response["author"]).to be_nil
14
+ end
15
+ end
16
+
7
17
  describe "with a regular user" do
8
18
  let(:author) { create(:user, organization: model.participatory_space.organization) }
9
19
  let(:query) { "{ author { name } }" }
@@ -72,6 +72,7 @@ shared_examples_for "coauthorable" do
72
72
  expect(coauthorable.authored_by?(creator_author)).to be(false)
73
73
  end
74
74
  end
75
+
75
76
  context "when the checked user is one of the coauthors" do
76
77
  before do
77
78
  other_authors.each { |author| coauthorable.authors << author }
@@ -92,6 +93,7 @@ shared_examples_for "coauthorable" do
92
93
  it "returns an empty list" do
93
94
  end
94
95
  end
96
+
95
97
  context "when there are many coauthors of both types" do
96
98
  before do
97
99
  other_authors.each { |author| coauthorable.authors << author }
@@ -99,6 +101,7 @@ shared_examples_for "coauthorable" do
99
101
  Decidim::Coauthorship.create(author: user_group.memberships.first.user, user_group: user_group, coauthorable: coauthorable)
100
102
  end
101
103
  end
104
+
102
105
  it "returns an array of identities" do
103
106
  identities = [creator_author]
104
107
  identities += other_authors
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples "editable content for admins" do
4
+ describe "edit link" do
5
+ before do
6
+ relogin_as user
7
+ visit current_path
8
+ end
9
+
10
+ context "when I'm an admin user" do
11
+ let(:user) { create(:user, :admin, :confirmed, organization: organization) }
12
+
13
+ it "has a link to edit the content at the admin" do
14
+ within ".topbar" do
15
+ expect(page).to have_link("Edit", href: /admin/)
16
+ end
17
+ end
18
+ end
19
+
20
+ context "when I'm a regular user" do
21
+ let(:user) { create(:user, :confirmed, organization: organization) }
22
+
23
+ it "does not have a link to edit the content at the admin" do
24
+ within ".topbar" do
25
+ expect(page).not_to have_link("Edit")
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ shared_examples_for "has space in m-cell" do
6
+ context "when rendering with show_space flag" do
7
+ # expects the cell to be invoked with a :show_space context flag that takes this value.
8
+ let(:show_space) { true }
9
+
10
+ it "renders the space where the model belongs to" do
11
+ expect(cell_html).to have_selector(".card__top .card__content.text-small")
12
+ expect(cell_html).to have_content(translated(model.component.participatory_space.title, locale: :en))
13
+ end
14
+ end
15
+ end
@@ -6,7 +6,7 @@ shared_examples_for "publicable" do
6
6
  let(:factory_name) { described_class.name.demodulize.underscore.to_sym }
7
7
 
8
8
  let!(:published) do
9
- create(factory_name, published_at: Time.zone.now)
9
+ create(factory_name, published_at: Time.current)
10
10
  end
11
11
 
12
12
  let!(:unpublished) do
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples_for "decidim module task loading" do |decidim_module|
4
+ let(:railties) do
5
+ Rails.application.railties
6
+ end
7
+
8
+ let(:railties_that_load_tasks) do
9
+ railties.select { |r| r.railtie_name.match?(decidim_module) && !r.paths["lib/tasks"].to_a.empty? }
10
+ end
11
+
12
+ it "loads tasks just under one railtie" do
13
+ expect(railties_that_load_tasks.size).to eq(1)
14
+ end
15
+ end
@@ -13,6 +13,7 @@ shared_examples "scope helpers" do
13
13
  before do
14
14
  allow(helper).to receive(:current_participatory_space).and_return(participatory_space)
15
15
  end
16
+
16
17
  let(:helper) do
17
18
  Class.new.tap do |v|
18
19
  v.extend(Decidim::ScopesHelper)
@@ -4,7 +4,7 @@ module Decidim
4
4
  # This holds the decidim-core version.
5
5
  module Core
6
6
  def self.version
7
- "0.13.1"
7
+ "0.14.1"
8
8
  end
9
9
  end
10
10
  end
@@ -73,7 +73,7 @@ module Decidim
73
73
  def notifiable?
74
74
  return false if resource.is_a?(Decidim::Publicable) && !resource.published?
75
75
  return false if participatory_space.is_a?(Decidim::Publicable) && !participatory_space&.published?
76
- return false unless component&.published?
76
+ return false if component && !component.published?
77
77
 
78
78
  return false if participatory_space.is_a?(Decidim::Participable) && !participatory_space.can_participate?(user)
79
79
 
@@ -90,6 +90,7 @@ module Decidim
90
90
  end
91
91
 
92
92
  def participatory_space
93
+ return resource if resource.is_a?(Decidim::ParticipatorySpaceResourceable)
93
94
  component&.participatory_space
94
95
  end
95
96
 
@@ -71,7 +71,13 @@ module Decidim
71
71
  locales.each_with_index.inject("".html_safe) do |string, (locale, index)|
72
72
  tab_content_id = "#{tabs_id}-#{name}-panel-#{index}"
73
73
  string + content_tag(:div, class: tab_element_class_for("panel", index), id: tab_content_id) do
74
- send(type, name_with_locale(name, locale), options.merge(label: false))
74
+ if options[:hashtaggable]
75
+ content_tag(:div, class: "hashtags__container") do
76
+ send(type, name_with_locale(name, locale), options.merge(label: false))
77
+ end
78
+ else
79
+ send(type, name_with_locale(name, locale), options.merge(label: false))
80
+ end
75
81
  end
76
82
  end
77
83
  end
@@ -137,11 +143,11 @@ module Decidim
137
143
  options[:lines] ||= 10
138
144
  options[:disabled] ||= false
139
145
 
140
- content_tag(:div, class: "editor") do
146
+ content_tag(:div, class: "editor #{"hashtags__container" if options[:hashtaggable]}") do
141
147
  template = ""
142
148
  template += label(name, options[:label].to_s || name) if options[:label] != false
143
149
  template += hidden_field(name, options)
144
- template += content_tag(:div, nil, class: "editor-container", data: {
150
+ template += content_tag(:div, nil, class: "editor-container #{"js-hashtags" if options[:hashtaggable]}", data: {
145
151
  toolbar: options[:toolbar],
146
152
  disabled: options[:disabled]
147
153
  }, style: "height: #{options[:lines]}rem")
@@ -10,7 +10,7 @@ module Decidim
10
10
 
11
11
  # Returns the creation date in a friendly relative format.
12
12
  def friendly_created_at
13
- current_datetime = Time.zone.now
13
+ current_datetime = Time.current
14
14
 
15
15
  if created_at > current_datetime.beginning_of_day
16
16
  I18n.l(created_at, format: :time_of_day)
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Gamification
5
+ autoload :Badge, "decidim/gamification/badge"
6
+ autoload :BadgeRegistry, "decidim/gamification/badge_registry"
7
+ autoload :BadgeStatus, "decidim/gamification/badge_status"
8
+ autoload :BadgeScorer, "decidim/gamification/badge_scorer"
9
+ autoload :BaseEvent, "decidim/gamification/base_event"
10
+ autoload :BadgeEarnedEvent, "decidim/gamification/badge_earned_event"
11
+ autoload :LevelUpEvent, "decidim/gamification/level_up_event"
12
+
13
+ # Public: Returns a the status of a badge given a user and a badge name.
14
+ #
15
+ # Returns a `BadgeStatus` instance.
16
+ def self.status_for(user, badge_name)
17
+ BadgeStatus.new(user, find_badge(badge_name))
18
+ end
19
+
20
+ # Public: Increments the score of a user for a badge.
21
+ #
22
+ # user - A User for whom to increase the score.
23
+ # badge_name - The name of the badge for which to increase the score.
24
+ # amount - (Optional) The amount to increase. Defaults to 1.
25
+ #
26
+ # Returns nothing.
27
+ def self.increment_score(user, badge_name, amount = 1)
28
+ BadgeScorer.new(user, find_badge(badge_name)).increment(amount)
29
+ end
30
+
31
+ # Public: Decrement the score of a user for a badge.
32
+ #
33
+ # user - A User for whom to increase the score.
34
+ # badge_name - The name of the badge for which to increase the score.
35
+ # amount - (Optional) The amount to decrease. Defaults to 1.
36
+ #
37
+ # Returns nothing.
38
+ def self.decrement_score(user, badge_name, amount = 1)
39
+ BadgeScorer.new(user, find_badge(badge_name)).decrement(amount)
40
+ end
41
+
42
+ # Public: Sets the score of a user for a badge.
43
+ #
44
+ # user - A User for whom to set the score.
45
+ # badge_name - The name of the badge for which to increase the score.
46
+ # score - The score to set.
47
+ #
48
+ # Returns nothing.
49
+ def self.set_score(user, badge_name, score)
50
+ BadgeScorer.new(user, find_badge(badge_name)).set(score)
51
+ end
52
+
53
+ # Semi-private: The BadgeRegistry to register global badges to.
54
+ def self.badge_registry
55
+ @badge_registry ||= Decidim::Gamification::BadgeRegistry.new
56
+ end
57
+
58
+ # Public: Returns all available badges.
59
+ #
60
+ # Returns an Array<Badge>
61
+ def self.badges
62
+ badge_registry.all
63
+ end
64
+
65
+ # Public: Finds a Badge given a name.
66
+ #
67
+ # Returns a Badge if found, nil otherwise.
68
+ def self.find_badge(name)
69
+ badge_registry.find(name)
70
+ end
71
+
72
+ # Public: Registers a new Badge.
73
+ #
74
+ # Example:
75
+ #
76
+ # Decidim.register_badge(:foo) do |badge|
77
+ # badge.levels = [1, 10, 50]
78
+ # end
79
+ #
80
+ # Returns nothing if registered successfully, raises an exception
81
+ # otherwise.
82
+ def self.register_badge(name, &block)
83
+ badge_registry.register(name, &block)
84
+ end
85
+
86
+ # Public: Resets all the badge scores using each of the badges'
87
+ # reset methods (if available). This is useful if the badges ever get
88
+ # inconsistent.
89
+ #
90
+ # users - The Array of Users to reset the score.
91
+ #
92
+ # Returns nothing.
93
+ def self.reset_badges(users = nil)
94
+ users ||= User.all
95
+
96
+ badges.each do |badge|
97
+ Rails.logger.info "Resetting #{badge.name}..."
98
+
99
+ if badge.reset
100
+ users.find_each do |user|
101
+ set_score(user, badge.name, badge.reset.call(user))
102
+ end
103
+ else
104
+ Rails.logger.info "Badge can't be reset since it doesn't have a reset method."
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Gamification
5
+ # This class represents an abstract badge. Instances of this class can define
6
+ # different badge types with different rules such as gaining new levels, etc.
7
+ class Badge
8
+ include Virtus.model
9
+ include ActiveModel::Validations
10
+
11
+ # The name of the badge.
12
+ attribute :name, String
13
+
14
+ # An array of scores needed to reach a new level. For example, the array
15
+ # [1, 5, 10] represents 1 point to get to Level 1, 5 points to get to level 2,
16
+ # 10 points to get to level 3.
17
+ attribute :levels, Array, default: []
18
+
19
+ # (Optional) you can set a lambda in order to be able to reset the score of a
20
+ # badge if the progress gets lost somehow. The lambda receives a user as an
21
+ # argument.
22
+ #
23
+ # It might not be possible sometimes, so it's fine to leave it empty.
24
+ attribute :reset, Proc
25
+
26
+ validates :name, :levels, presence: true
27
+ validates :levels, empty: false
28
+
29
+ validate do
30
+ errors.add(:levels, "level thresholds should be ordered") if levels.sort != levels
31
+ errors.add(:levels, "level thresholds should be positive") unless levels.all?(&:positive?)
32
+ errors.add(:levels, "level thresholds should be unique") unless levels.uniq == levels
33
+ end
34
+
35
+ # Public: Returns the level for this badge given a score.
36
+ #
37
+ # Returns an Integer with the level.
38
+ def level_of(score)
39
+ levels.each_with_index do |threshold, index|
40
+ return index if threshold > score
41
+ end
42
+
43
+ levels.length
44
+ end
45
+
46
+ # Public: Returns an image for this badge.
47
+ #
48
+ # Returns a String with the image.
49
+ def image
50
+ ActionController::Base.helpers.asset_path("decidim/gamification/badges/#{name}.svg")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,9 @@
1
+ # frozen-string_literal: true
2
+
3
+ module Decidim
4
+ module Gamification
5
+ # This event is sent when a badge is earned for the first time.
6
+ class BadgeEarnedEvent < BaseEvent
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Gamification
5
+ # This class represents a repository of badges. New badges can be
6
+ # registered thanks to its DSL and will be validated prior to being
7
+ # inserted.
8
+ #
9
+ class BadgeRegistry
10
+ # Public: Initializes the badge registry.
11
+ def initialize
12
+ @badges = {}
13
+ end
14
+
15
+ # Public: Returns all the registered badges.
16
+ #
17
+ # Returns Array<Badge>.
18
+ def all
19
+ @badges.values
20
+ end
21
+
22
+ # Public: Finds a badge given its name.
23
+ #
24
+ # name - The name of the badge to find.
25
+ #
26
+ # Returns a Badge if found or nil otherwise.
27
+ def find(name)
28
+ @badges[name.to_s]
29
+ end
30
+
31
+ # Public: Registers a new badge.
32
+ #
33
+ # name - The name of the badge to register.
34
+ # &block - A block that gets the new badge as argument.
35
+ #
36
+ # Example:
37
+ # register(:fake){ |badge| badge.levels = [1, 3, 10] }
38
+ #
39
+ # Returns a Badge when registered successfully, raises an exception
40
+ # otherwise.
41
+ def register(name, &block)
42
+ name = name.to_s
43
+
44
+ badge = Badge.new(name: name).tap do |object|
45
+ object.instance_eval(&block)
46
+ end
47
+
48
+ badge.validate!
49
+
50
+ @badges[name] = badge
51
+ end
52
+
53
+ # Public: Unregisters a previously registered badge.
54
+ #
55
+ # name - The name of the badge to unregister.
56
+ #
57
+ # Returns the deleted Badge if found, nil otherwise.
58
+ def unregister(name)
59
+ @badges.delete(name.to_s)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Gamification
5
+ # This class is responsible of updating scores given a user and a badge. Will
6
+ # also trigger any side-effects such as notifications.
7
+ class BadgeScorer
8
+ # Public: Initializes the class.
9
+ #
10
+ # user - The user for which to update scores.
11
+ # badge - The `Badge` to update.
12
+ #
13
+ def initialize(user, badge)
14
+ @user = user
15
+ @badge = badge
16
+ end
17
+
18
+ # Public: Increments the score for the user and badge.
19
+ #
20
+ # amount - Amount to increment. Defaults to 1.
21
+ #
22
+ # Returns a `BadgeScore`.
23
+ def increment(amount = 1)
24
+ raise InvalidAmountException unless amount.positive?
25
+
26
+ with_level_tracking do
27
+ BadgeScore.find_or_create_by(
28
+ user: @user,
29
+ badge_name: @badge.name
30
+ ).increment(:value, amount).save!
31
+ end
32
+ end
33
+
34
+ # Public: Decrements the score for the user and badge.
35
+ #
36
+ # amount - Amount to decrement. Defaults to 1.
37
+ #
38
+ # Returns a `BadgeScore`.
39
+ def decrement(amount = 1)
40
+ raise InvalidAmountException unless amount.positive?
41
+
42
+ with_level_tracking do
43
+ badge_score = BadgeScore.find_by(
44
+ user: @user,
45
+ badge_name: @badge.name
46
+ )
47
+
48
+ next if badge_score.blank?
49
+
50
+ badge_score.decrement(:value, amount)
51
+ badge_score.value = 0 if badge_score.value.negative?
52
+ badge_score.save!
53
+ end
54
+ end
55
+
56
+ # Public: Sets the score for the user and badge.
57
+ #
58
+ # score - Score to set.
59
+ #
60
+ # Returns a `BadgeScore`.
61
+ def set(score)
62
+ raise NegativeScoreException if score.negative?
63
+
64
+ with_level_tracking do
65
+ BadgeScore.find_or_create_by(
66
+ user_id: @user.id,
67
+ badge_name: @badge.name
68
+ ).update!(value: score)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ class NegativeScoreException < StandardError; end
75
+ class InvalidAmountException < StandardError; end
76
+
77
+ def with_level_tracking
78
+ previous_level = BadgeStatus.new(@user, @badge).level
79
+
80
+ yield
81
+
82
+ current_status = BadgeStatus.new(@user, @badge)
83
+ send_notification(previous_level, current_status.level)
84
+ current_status
85
+ end
86
+
87
+ def send_notification(previous_level, current_level)
88
+ return unless current_level > previous_level
89
+
90
+ if previous_level.zero?
91
+ publish_event(name: "decidim.events.gamification.badge_earned",
92
+ klass: BadgeEarnedEvent,
93
+ previous_level: previous_level,
94
+ current_level: current_level)
95
+ else
96
+ publish_event(name: "decidim.events.gamification.level_up",
97
+ klass: LevelUpEvent,
98
+ previous_level: previous_level,
99
+ current_level: current_level)
100
+ end
101
+ end
102
+
103
+ def publish_event(name:, klass:, previous_level:, current_level:)
104
+ Decidim::EventsManager.publish(
105
+ event: name,
106
+ event_class: klass,
107
+ resource: @user,
108
+ recipient_ids: [@user.id],
109
+ extra: {
110
+ badge_name: @badge.name.to_s,
111
+ previous_level: previous_level,
112
+ current_level: current_level
113
+ }
114
+ )
115
+ end
116
+ end
117
+ end
118
+ end