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.
- checksums.yaml +4 -4
- data/app/assets/config/decidim_core_manifest.js +1 -1
- data/app/assets/images/decidim/gamification/badges/invitations.svg +117 -0
- data/app/assets/javascripts/decidim.js.es6 +4 -1
- data/app/assets/javascripts/decidim/ajax_modals.js.es6 +17 -0
- data/app/assets/javascripts/decidim/conferences.js.es6 +16 -0
- data/app/assets/javascripts/decidim/input_hashtags.js.es6 +115 -0
- data/app/assets/javascripts/decidim/input_mentions.js.es6 +2 -3
- data/app/assets/javascripts/decidim/vizzs/areachart.js.es6 +226 -0
- data/app/assets/javascripts/decidim/vizzs/metrics.js.es6 +26 -0
- data/app/assets/javascripts/decidim/vizzs/orgchart.js.es6 +701 -0
- data/app/assets/javascripts/decidim/vizzs/renders.js.es6 +11 -0
- data/app/assets/stylesheets/decidim/extras/_proposal_form.scss +3 -1
- data/app/assets/stylesheets/decidim/layouts/_home.scss +1 -1
- data/app/assets/stylesheets/decidim/modules/_areachart.scss +74 -0
- data/app/assets/stylesheets/decidim/modules/_badges.scss +116 -0
- data/app/assets/stylesheets/decidim/modules/_buttons.scss +5 -0
- data/app/assets/stylesheets/decidim/modules/_cards.scss +21 -4
- data/app/assets/stylesheets/decidim/modules/_chart-tooltip.scss +42 -0
- data/app/assets/stylesheets/decidim/modules/_collapsible-list.scss +12 -8
- data/app/assets/stylesheets/decidim/modules/_conference-nav.scss +31 -0
- data/app/assets/stylesheets/decidim/modules/_conference-programme.scss +110 -0
- data/app/assets/stylesheets/decidim/modules/_conference-speaker.scss +86 -0
- data/app/assets/stylesheets/decidim/modules/_conversation.scss +58 -0
- data/app/assets/stylesheets/decidim/modules/_help.scss +38 -0
- data/app/assets/stylesheets/decidim/modules/_hover-section.scss +29 -0
- data/app/assets/stylesheets/decidim/modules/_icons.scss +10 -4
- data/app/assets/stylesheets/decidim/modules/_input-hashtags.scss +124 -0
- data/app/assets/stylesheets/decidim/modules/_loading-spinner.scss +12 -0
- data/app/assets/stylesheets/decidim/modules/_margins.scss +2 -2
- data/app/assets/stylesheets/decidim/modules/_modules.scss +15 -0
- data/app/assets/stylesheets/decidim/modules/_navbar.scss +9 -0
- data/app/assets/stylesheets/decidim/modules/_orgchart.scss +62 -0
- data/app/assets/stylesheets/decidim/modules/_status-labels.scss +2 -1
- data/app/assets/stylesheets/decidim/modules/_typography.scss +9 -0
- data/app/assets/stylesheets/decidim/utils/_helpers.scss +28 -0
- data/app/assets/stylesheets/decidim/utils/_mixins.scss +63 -0
- data/app/cells/decidim/author/withdraw.erb +1 -1
- data/app/cells/decidim/author_cell.rb +1 -1
- data/app/cells/decidim/badge/show.erb +36 -0
- data/app/cells/decidim/badge_cell.rb +53 -0
- data/app/cells/decidim/badges/show.erb +6 -0
- data/app/cells/decidim/badges_cell.rb +14 -0
- data/app/cells/decidim/card_m/header.erb +1 -1
- data/app/cells/decidim/card_m/show.erb +1 -2
- data/app/cells/decidim/card_m/top.erb +7 -0
- data/app/cells/decidim/card_m_cell.rb +14 -17
- data/app/cells/decidim/coauthorships_cell.rb +77 -0
- data/app/cells/decidim/collapsible_authors/show.erb +0 -1
- data/app/cells/decidim/collapsible_authors_cell.rb +4 -4
- data/app/cells/decidim/collapsible_list/show.erb +12 -4
- data/app/cells/decidim/collapsible_list_cell.rb +14 -12
- data/app/cells/decidim/content_blocks/footer_sub_hero/show.erb +14 -0
- data/app/cells/decidim/content_blocks/footer_sub_hero_cell.rb +12 -0
- data/app/{views/decidim/pages/home/_hero.html.erb → cells/decidim/content_blocks/hero/show.erb} +4 -4
- data/app/cells/decidim/content_blocks/hero_cell.rb +25 -0
- data/app/cells/decidim/content_blocks/hero_settings_form/show.erb +7 -0
- data/app/cells/decidim/content_blocks/hero_settings_form_cell.rb +13 -0
- data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +24 -0
- data/app/cells/decidim/content_blocks/highlighted_content_banner_cell.rb +16 -0
- data/app/{views/decidim/pages/home/_extended.html.erb → cells/decidim/content_blocks/how_to_participate/show.erb} +10 -10
- data/app/cells/decidim/content_blocks/how_to_participate_cell.rb +9 -0
- data/app/{views/decidim/pages/home/_statistics.html.erb → cells/decidim/content_blocks/stats/show.erb} +2 -2
- data/app/cells/decidim/content_blocks/stats_cell.rb +18 -0
- data/app/{views/decidim/pages/home/_sub_hero.html.erb → cells/decidim/content_blocks/sub_hero/show.erb} +2 -2
- data/app/cells/decidim/content_blocks/sub_hero_cell.rb +17 -0
- data/app/cells/decidim/conversation/show.erb +18 -0
- data/app/cells/decidim/conversation_cell.rb +23 -0
- data/app/cells/decidim/conversation_header/show.erb +17 -0
- data/app/cells/decidim/conversation_header_cell.rb +16 -0
- data/app/cells/decidim/conversations/show.erb +45 -0
- data/app/cells/decidim/conversations_cell.rb +24 -0
- data/app/cells/decidim/follow_button/show.erb +3 -3
- data/app/cells/decidim/follow_button_cell.rb +1 -5
- data/app/cells/decidim/following_cell.rb +1 -7
- data/app/cells/decidim/message/show.erb +15 -0
- data/app/cells/decidim/message_cell.rb +23 -0
- data/app/cells/decidim/new_conversation/show.erb +19 -0
- data/app/cells/decidim/new_conversation_cell.rb +19 -0
- data/app/cells/decidim/notifications/show.erb +1 -1
- data/app/cells/decidim/profile/show.erb +27 -0
- data/app/cells/decidim/profile_cell.rb +33 -0
- data/app/cells/decidim/profile_sidebar/show.erb +57 -0
- data/app/cells/decidim/profile_sidebar_cell.rb +31 -0
- data/app/cells/decidim/tos_page_cell.rb +0 -4
- data/app/cells/decidim/user_profile/header.erb +1 -1
- data/app/controllers/concerns/decidim/action_authorization.rb +13 -38
- data/app/controllers/concerns/decidim/needs_permission.rb +15 -6
- data/app/controllers/decidim/application_controller.rb +1 -0
- data/app/controllers/decidim/authorization_modals_controller.rb +35 -0
- data/app/controllers/decidim/components/base_controller.rb +0 -1
- data/app/controllers/decidim/devise/invitations_controller.rb +2 -1
- data/app/controllers/decidim/messaging/conversations_controller.rb +2 -11
- data/app/controllers/decidim/newsletters_controller.rb +4 -6
- data/app/controllers/decidim/notifications_controller.rb +4 -0
- data/app/controllers/decidim/pages_controller.rb +3 -7
- data/app/controllers/decidim/profiles_controller.rb +17 -7
- data/app/forms/decidim/notifications_settings_form.rb +1 -1
- data/app/forms/decidim/registration_form.rb +1 -1
- data/app/helpers/decidim/action_authorization_helper.rb +51 -46
- data/app/helpers/decidim/application_helper.rb +18 -0
- data/app/helpers/decidim/card_helper.rb +1 -1
- data/app/helpers/decidim/cells_helper.rb +6 -2
- data/app/helpers/decidim/resource_helper.rb +8 -1
- data/app/helpers/decidim/searches_helper.rb +5 -4
- data/app/helpers/decidim/traceability_helper.rb +5 -1
- data/app/models/decidim/authorization.rb +2 -2
- data/app/models/decidim/content_block.rb +144 -0
- data/app/models/decidim/gamification/badge_score.rb +13 -0
- data/app/models/decidim/messaging/message.rb +1 -1
- data/app/models/decidim/messaging/receipt.rb +1 -1
- data/app/models/decidim/organization.rb +1 -5
- data/app/models/decidim/resource_permission.rb +8 -0
- data/app/models/decidim/searchable_resource.rb +1 -1
- data/app/models/decidim/user.rb +17 -1
- data/app/permissions/decidim/default_permissions.rb +4 -3
- data/app/permissions/decidim/permissions.rb +33 -1
- data/app/presenters/decidim/hashtag_presenter.rb +32 -0
- data/app/presenters/decidim/resource_locator_presenter.rb +13 -0
- data/app/presenters/decidim/user_presenter.rb +1 -1
- data/app/queries/decidim/messaging/user_conversations.rb +1 -1
- data/app/resolvers/decidim/hashtags_resolver.rb +15 -0
- data/app/services/decidim/action_authorizer.rb +9 -8
- data/app/types/decidim/core/date_time_type.rb +1 -1
- data/app/types/decidim/core/hashtag_type.rb +13 -0
- data/app/uploaders/decidim/homepage_image_uploader.rb +1 -1
- data/app/uploaders/decidim/image_uploader.rb +1 -0
- data/app/views/decidim/authorization_modals/show.html.erb +32 -0
- data/app/views/decidim/messaging/conversations/create.js.erb +1 -1
- data/app/views/decidim/messaging/conversations/index.html.erb +1 -51
- data/app/views/decidim/messaging/conversations/new.html.erb +1 -5
- data/app/views/decidim/messaging/conversations/show.html.erb +1 -9
- data/app/views/decidim/messaging/conversations/update.js.erb +1 -1
- data/app/views/decidim/notifications/index.html.erb +1 -0
- data/app/views/decidim/pages/decidim_page.html.erb +9 -0
- data/app/views/decidim/pages/home.html.erb +12 -16
- data/app/views/decidim/pages/index.html.erb +8 -0
- data/app/views/decidim/profiles/_user_follow.erb +2 -2
- data/app/views/decidim/profiles/show.html.erb +1 -37
- data/app/views/decidim/searches/_results.html.erb +1 -1
- data/app/views/decidim/shared/_author_reference.html.erb +1 -1
- data/app/views/decidim/shared/_authorization_modal.html.erb +1 -0
- data/app/views/decidim/shared/_tags.html.erb +1 -1
- data/app/views/kaminari/decidim/_page.html.erb +1 -1
- data/app/views/layouts/decidim/_application.html.erb +6 -1
- data/app/views/layouts/decidim/_edit_link.html.erb +8 -0
- data/app/views/layouts/decidim/_impersonation_warning.html.erb +1 -1
- data/app/views/layouts/decidim/_user_menu.html.erb +2 -2
- data/app/views/layouts/decidim/_wrapper.html.erb +14 -1
- data/config/initializers/carrierwave.rb +15 -0
- data/config/locales/ca.yml +78 -30
- data/config/locales/en.yml +78 -30
- data/config/locales/es-PY.yml +78 -30
- data/config/locales/es.yml +78 -30
- data/config/locales/eu.yml +78 -30
- data/config/locales/fi.yml +262 -214
- data/config/locales/fr.yml +78 -30
- data/config/locales/gl.yml +78 -30
- data/config/locales/hu.yml +781 -0
- data/config/locales/it.yml +78 -30
- data/config/locales/nl.yml +78 -30
- data/config/locales/pl.yml +78 -30
- data/config/locales/pt-BR.yml +106 -58
- data/config/locales/pt.yml +78 -30
- data/config/locales/ru.yml +52 -32
- data/config/locales/sv.yml +183 -135
- data/config/locales/uk.yml +60 -40
- data/config/routes.rb +8 -6
- data/db/migrate/20180705091019_create_decidim_resource_permissions.rb +12 -0
- data/db/migrate/20180706104107_add_nickname_to_managed_users.rb +14 -0
- data/db/migrate/20180706111847_fix_result_follows.rb +9 -0
- data/db/migrate/20180724103814_add_content_blocks.rb +22 -0
- data/db/migrate/20180726112510_create_decidim_hashtags.rb +17 -0
- data/db/migrate/20180730071851_add_core_content_blocks.rb +28 -0
- data/db/migrate/20180802132147_rename_content_block_options_to_settings.rb +7 -0
- data/db/migrate/20180806095628_add_badge_scores.rb +11 -0
- data/db/migrate/20180808135006_add_images_to_content_blocks.rb +7 -0
- data/db/migrate/20180810092428_move_organization_fields_to_hero_content_block.rb +23 -0
- data/db/seeds.rb +10 -2
- data/lib/decidim/api/authorable_interface.rb +1 -1
- data/lib/decidim/coauthorable.rb +1 -0
- data/lib/decidim/content_block_manifest.rb +58 -0
- data/lib/decidim/content_block_registry.rb +87 -0
- data/lib/decidim/content_parsers.rb +1 -0
- data/lib/decidim/content_parsers/hashtag_parser.rb +36 -0
- data/lib/decidim/content_processor.rb +11 -0
- data/lib/decidim/content_renderers.rb +1 -0
- data/lib/decidim/content_renderers/hashtag_renderer.rb +43 -0
- data/lib/decidim/core.rb +28 -6
- data/lib/decidim/core/api.rb +1 -0
- data/lib/decidim/core/engine.rb +52 -1
- data/lib/decidim/core/test.rb +3 -0
- data/lib/decidim/core/test/factories.rb +32 -17
- data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +10 -0
- data/lib/decidim/core/test/shared_examples/coauthorable.rb +3 -0
- data/lib/decidim/core/test/shared_examples/edit_link_shared_examples.rb +30 -0
- data/lib/decidim/core/test/shared_examples/has_space_in_mcell_examples.rb +15 -0
- data/lib/decidim/core/test/shared_examples/publicable.rb +1 -1
- data/lib/decidim/core/test/shared_examples/railtie_examples.rb +15 -0
- data/lib/decidim/core/test/shared_examples/scope_helper_examples.rb +1 -0
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/events/base_event.rb +2 -1
- data/lib/decidim/form_builder.rb +9 -3
- data/lib/decidim/friendly_dates.rb +1 -1
- data/lib/decidim/gamification.rb +109 -0
- data/lib/decidim/gamification/badge.rb +54 -0
- data/lib/decidim/gamification/badge_earned_event.rb +9 -0
- data/lib/decidim/gamification/badge_registry.rb +63 -0
- data/lib/decidim/gamification/badge_scorer.rb +118 -0
- data/lib/decidim/gamification/badge_status.rb +41 -0
- data/lib/decidim/gamification/base_event.rb +40 -0
- data/lib/decidim/gamification/level_up_event.rb +9 -0
- data/lib/decidim/hashtag.rb +15 -0
- data/lib/decidim/hashtaggable.rb +20 -0
- data/lib/decidim/query_extensions.rb +10 -0
- data/lib/decidim/resource_manifest.rb +10 -0
- data/lib/decidim/resourceable.rb +13 -0
- data/lib/decidim/search_resource_fields_mapper.rb +8 -3
- data/lib/decidim/searchable.rb +8 -0
- data/lib/decidim/translatable_attributes.rb +6 -18
- data/lib/decidim/view_model.rb +6 -0
- data/lib/devise/models/decidim_newsletterable.rb +1 -1
- data/vendor/assets/javascripts/d3.js +17813 -0
- metadata +125 -27
- data/app/cells/decidim/card_m/author.erb +0 -3
- data/app/cells/decidim/card_m/authors.erb +0 -9
- data/app/views/decidim/messaging/conversations/_message.html.erb +0 -14
- data/app/views/decidim/messaging/conversations/_reply.html.erb +0 -11
- data/app/views/decidim/messaging/conversations/_show.html.erb +0 -21
- data/app/views/decidim/messaging/conversations/_start.html.erb +0 -12
- data/app/views/decidim/pages/home/_footer_sub_hero.html.erb +0 -14
- data/app/views/decidim/pages/home/_highlighted_content_banner.html.erb +0 -26
- data/app/views/decidim/pages/home/_highlighted_processes.html.erb +0 -7
- data/app/views/decidim/profiles/_user.html.erb +0 -59
- data/app/views/decidim/shared/_action_authorization_modal.html.erb +0 -39
- 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.
|
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
|
data/lib/decidim/core/version.rb
CHANGED
@@ -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
|
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
|
|
data/lib/decidim/form_builder.rb
CHANGED
@@ -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
|
-
|
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.
|
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,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
|