Skip to content

Commit c98c244

Browse files
microstudiElviaBth
andcommitted
Improve components and spaces sharing with administrable tokens (decidim#13221)
* remove share_tokens from admin/components/_form * add routes * add component name on index title * add form and new views * fix index styles * refactor share_token form * refactor share_token form * add locales keys * add automatic_token attribute * refactor createsharetoken command * remove target_blank * add more methods to sharetokenform * refactor token_for definition * add edit and update method to controller * fix routing error * add create and update command specs * fix spelling error * fix spelling error * add share_token_form spec * remove target_ blank from edit action * remove set_default_expiration method * add collection_radio_buttonts * fix no expitration bug * add registered_only to decidim_share_tokens migration * add more checks to share_tokens_form spec * add more checks to commands create and update share_token spec * add copy clipboard functionality * fix lint errors * add is_active_link for share_tokens_path * remove space detected * fix noMethodError on user_can_preview_component? method * add enforce_permission to controller * fix manage_components_share_tokens spec * fix manage_process_components_examples spec * fix share_token_spec * add share_tokens routes on initiatives-module * remove unused i18n keys * add more checks to manage_component_share_tokens * fix has to edit a share token case check * add spec check allows copying the share link from the share token * save clipboard-copy-label-original * fix clipboard js * fix validations and views * add specs * update permissions * update documentation * add help text * allow to manage participatory spaces share tokens * add space specs * add preview specs * fix clipboard * fix specs * fix new minimum page items * trailing spaces * use standard datepicker * fix surveys component actions * make spec deterministic * fix specs * debug * prevent token repetition in parallel tests * apply corrections * fix typo * fix typo * harmonize copies * apply review * lint * change test * apply review * add td sizes * normalize sizes * add action logs * add model specs * fix title interpolation * fix creat command spec --------- Co-authored-by: elviabth <ejbenedith@gmail.com>
1 parent b0b0553 commit c98c244

93 files changed

Lines changed: 2005 additions & 302 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

decidim-accountability/spec/system/preview_accountability_with_share_token_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
require "spec_helper"
44

5-
describe "Preview accountability with share token" do
5+
describe "preview accountability with a share token" do
66
let(:manifest_name) { "accountability" }
77

88
include_context "with a component"
9-
it_behaves_like "preview component with share_token"
9+
it_behaves_like "preview component with a share_token"
1010
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
module Decidim
4+
module Admin
5+
# A command with all the business logic to create a share token.
6+
# This command is called from the controller.
7+
class CreateShareToken < Decidim::Commands::CreateResource
8+
fetch_form_attributes :token, :expires_at, :registered_only, :organization, :user, :token_for
9+
10+
protected
11+
12+
def resource_class = Decidim::ShareToken
13+
14+
def extra_params
15+
{
16+
participatory_space: {
17+
title: participatory_space&.title
18+
},
19+
resource: {
20+
title: component&.name
21+
}
22+
}
23+
end
24+
25+
def participatory_space
26+
return form.token_for if form.token_for.try(:manifest).is_a?(Decidim::ParticipatorySpaceManifest)
27+
return current_participatory_space if respond_to?(:current_participatory_space)
28+
29+
component&.participatory_space
30+
end
31+
32+
def component
33+
return form.token_for if form.token_for.is_a?(Decidim::Component)
34+
35+
form.token_for.try(:component)
36+
end
37+
end
38+
end
39+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module Decidim
4+
module Admin
5+
# A command with all the business logic to destroy a share token.
6+
# This command is called from the controller.
7+
class DestroyShareToken < Decidim::Commands::DestroyResource
8+
delegate :participatory_space, :component, to: :resource
9+
10+
def extra_params
11+
{
12+
participatory_space: {
13+
title: participatory_space&.title
14+
},
15+
resource: {
16+
title: component&.name
17+
}
18+
}
19+
end
20+
end
21+
end
22+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module Decidim
4+
module Admin
5+
# A command with all the business logic to update a share token.
6+
# This command is called from the controller.
7+
class UpdateShareToken < Decidim::Commands::UpdateResource
8+
fetch_form_attributes :expires_at, :registered_only
9+
10+
delegate :participatory_space, :component, to: :resource
11+
12+
def extra_params
13+
{
14+
participatory_space: {
15+
title: participatory_space&.title
16+
},
17+
resource: {
18+
title: component&.name
19+
}
20+
}
21+
end
22+
end
23+
end
24+
end

decidim-admin/app/controllers/decidim/admin/share_tokens_controller.rb

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,66 @@
22

33
module Decidim
44
module Admin
5+
# This is an abstract controller allows sharing unpublished things.
6+
# Final implementation must inherit from this controller and implement the `resource` method.
57
class ShareTokensController < Decidim::Admin::ApplicationController
8+
include Decidim::Admin::Filterable
9+
10+
helper_method :current_token, :resource, :resource_title, :share_tokens_path
11+
12+
def index
13+
enforce_permission_to :read, :share_tokens
14+
@share_tokens = filtered_collection
15+
end
16+
17+
def new
18+
enforce_permission_to :create, :share_tokens
19+
@form = form(ShareTokenForm).instance
20+
end
21+
22+
def create
23+
enforce_permission_to :create, :share_tokens
24+
@form = form(ShareTokenForm).from_params(params, resource:)
25+
26+
CreateShareToken.call(@form) do
27+
on(:ok) do
28+
flash[:notice] = I18n.t("share_tokens.create.success", scope: "decidim.admin")
29+
redirect_to share_tokens_path
30+
end
31+
32+
on(:invalid) do
33+
flash.now[:alert] = I18n.t("share_tokens.create.invalid", scope: "decidim.admin")
34+
render action: "new"
35+
end
36+
end
37+
end
38+
39+
def edit
40+
enforce_permission_to(:update, :share_tokens, share_token: current_token)
41+
@form = form(ShareTokenForm).from_model(current_token)
42+
end
43+
44+
def update
45+
enforce_permission_to(:update, :share_tokens, share_token: current_token)
46+
@form = form(ShareTokenForm).from_params(params, resource:)
47+
48+
UpdateShareToken.call(@form, current_token) do
49+
on(:ok) do
50+
flash[:notice] = I18n.t("share_tokens.update.success", scope: "decidim.admin")
51+
redirect_to share_tokens_path
52+
end
53+
54+
on(:invalid) do
55+
flash.now[:alert] = I18n.t("share_tokens.update.error", scope: "decidim.admin")
56+
render :edit
57+
end
58+
end
59+
end
60+
661
def destroy
7-
enforce_permission_to(:destroy, :share_token, share_token:)
62+
enforce_permission_to(:destroy, :share_tokens, share_token: current_token)
863

9-
Decidim::Commands::DestroyResource.call(share_token, current_user) do
64+
DestroyShareToken.call(current_token, current_user) do
1065
on(:ok) do
1166
flash[:notice] = I18n.t("share_tokens.destroy.success", scope: "decidim.admin")
1267
end
@@ -15,15 +70,62 @@ def destroy
1570
end
1671
end
1772

18-
redirect_back(fallback_location: root_path)
73+
redirect_to share_tokens_path
1974
end
2075

2176
private
2277

23-
def share_token
24-
@share_token ||= Decidim::ShareToken.where(
25-
organization: current_organization
26-
).find(params[:id])
78+
# override this method in the destination controller to specify the resource associated with the shared token (ie: a component)
79+
def resource
80+
raise NotImplementedError
81+
end
82+
83+
# Override also this method if resource does not respond to a translatable name or title
84+
def resource_title
85+
translated_attribute(resource.try(:name) || resource.title)
86+
end
87+
88+
# sets the prefix for the route helper methods (this may vary depending on the resource type)
89+
# This setup works fine for participatory spaces and components, override if needed
90+
def route_name
91+
@route_name ||= "#{resource.manifest.route_name}_"
92+
end
93+
94+
def route_proxy
95+
@route_proxy ||= EngineRouter.admin_proxy(resource.try(:participatory_space) || resource)
96+
end
97+
98+
# returns the proper path for managing a share token according to the resource
99+
# this works fine for components and participatory spaces, override if needed
100+
def share_tokens_path(method = :index, options = {})
101+
args = resource.is_a?(Decidim::Component) ? [resource, options] : [options]
102+
103+
case method
104+
when :index, :create
105+
route_proxy.send("#{route_name}share_tokens_path", *args)
106+
when :new
107+
route_proxy.send("new_#{route_name}share_token_path", *args)
108+
when :update, :destroy
109+
route_proxy.send("#{route_name}share_token_path", *args)
110+
when :edit
111+
route_proxy.send("edit_#{route_name}share_token_path", *args)
112+
end
113+
end
114+
115+
def base_query
116+
collection
117+
end
118+
119+
def collection
120+
@collection ||= Decidim::ShareToken.where(organization: current_organization, token_for: resource)
121+
end
122+
123+
def filters
124+
[]
125+
end
126+
127+
def current_token
128+
@current_token ||= collection.find(params[:id])
27129
end
28130
end
29131
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
module Decidim
4+
module Admin
5+
class ShareTokenForm < Decidim::Form
6+
mimic :share_token
7+
8+
attribute :token, String
9+
attribute :automatic_token, Boolean, default: true
10+
attribute :expires_at, Decidim::Attributes::TimeWithZone
11+
attribute :no_expiration, Boolean, default: true
12+
attribute :registered_only, Boolean, default: false
13+
14+
validates :token, presence: true, if: ->(form) { form.automatic_token.blank? }
15+
validate :token_uniqueness, if: ->(form) { form.automatic_token.blank? }
16+
17+
validates_format_of :token, with: /\A[a-zA-Z0-9_-]+\z/, allow_blank: true
18+
validates :expires_at, presence: true, if: ->(form) { form.no_expiration.blank? }
19+
20+
def map_model(model)
21+
self.no_expiration = model.expires_at.blank?
22+
end
23+
24+
def token
25+
super.strip.upcase.gsub(/\s+/, "-") if super.present?
26+
end
27+
28+
def expires_at
29+
return nil if no_expiration.present?
30+
31+
super
32+
end
33+
34+
def token_for
35+
context[:resource]
36+
end
37+
38+
def organization
39+
context[:current_organization]
40+
end
41+
42+
def user
43+
context[:current_user]
44+
end
45+
46+
private
47+
48+
def token_uniqueness
49+
return unless Decidim::ShareToken.where(organization:, token_for:, token:).where.not(id:).any?
50+
51+
errors.add(:token, :taken)
52+
end
53+
end
54+
end
55+
end

decidim-admin/app/views/decidim/admin/components/_actions.html.erb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
<span class="action-space icon"></span>
55
<% end %>
66

7-
<% if allowed_to? :share, :component, component: component %>
8-
<%= icon_link_to "share-line", url_for(action: :share, id: component, controller: "components"), t("actions.share", scope: "decidim.admin"), target: :blank, class: "action-icon--share" %>
7+
<% if component.manifest.admin_engine && allowed_to?(:share, :component, component: component) %>
8+
<%= icon_link_to "share-line", component_share_tokens_path(component_id: component), t("actions.share_tokens", scope: "decidim.admin"), class: "action-icon--share" %>
99
<% else %>
1010
<span class="action-space icon"></span>
1111
<% end %>
12+
1213
<% if allowed_to? :update, :component, component: component %>
1314
<%= icon_link_to "settings-4-line", url_for(action: :edit, id: component, controller: "components"), t("actions.configure", scope: "decidim.admin"), class: "action-icon--configure" %>
1415
<% else %>

decidim-admin/app/views/decidim/admin/components/_form.html.erb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,4 @@
114114
</div>
115115
</div>
116116
<% end %>
117-
<% if component && component.persisted? && !component.published? %>
118-
<div class="card" data-component="accordion" id="accordion-share_tokens">
119-
<div id="panel-share_tokens" class="card-section">
120-
<div class="row column">
121-
<%= render partial: "decidim/admin/share_tokens/share_tokens", locals: { share_tokens: form.object.share_tokens } %>
122-
</div>
123-
</div>
124-
</div>
125-
<% end %>
126117
</div>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<div class="row column">
2+
<label><%= t("share_tokens.form.expires_at", scope: "decidim.admin") %></label>
3+
<%= form.collection_radio_buttons :no_expiration, [[true, t("share_tokens.form.never_expire", scope: "decidim.admin")], [false, t("share_tokens.form.custom", scope: "decidim.admin")]], :first, :last do |b| %>
4+
<div>
5+
<%= b.radio_button %>
6+
<%= b.label %>
7+
</div>
8+
<% end %>
9+
<div id="expires_at_field_wrapper" class="hidden mt-4 row column">
10+
<%= form.datetime_field :expires_at, label: t("share_tokens.form.custom_expiration", scope: "decidim.admin") %>
11+
</div>
12+
</div>
13+
14+
<div class="row column">
15+
<label><%= t("share_tokens.form.registered_only", scope: "decidim.admin") %></label>
16+
<%= form.collection_radio_buttons :registered_only, [
17+
[t("share_tokens.form.true", scope: "decidim.admin"), true],
18+
[t("share_tokens.form.false", scope: "decidim.admin"), false]
19+
], :last, :first do |b| %>
20+
<div>
21+
<%= b.label do %>
22+
<%= b.radio_button %>
23+
<%= b.text %>
24+
<% end %>
25+
</div>
26+
<% end %>
27+
</div>
28+
29+
<script>
30+
document.addEventListener("DOMContentLoaded", () => {
31+
const expiresButton = document.querySelector("input[name='share_token[no_expiration]'][value='false']");
32+
const expiresAtRadioButtons = document.querySelectorAll("input[name='share_token[no_expiration]']");
33+
const expiresAtWrapper = document.getElementById("expires_at_field_wrapper");
34+
const expiresAtInput = document.querySelector("input[name='share_token[expires_at]']");
35+
36+
const toggleExpiresAtField = () => {
37+
if (expiresButton.checked) {
38+
expiresAtWrapper.classList.remove("hidden");
39+
} else {
40+
expiresAtWrapper.classList.add("hidden");
41+
expiresAtInput.value = "";
42+
expiresAtInput.removeAttribute("required");
43+
}
44+
};
45+
46+
expiresAtRadioButtons.forEach(expiresAtRadioButton => {
47+
expiresAtRadioButton.addEventListener("change", toggleExpiresAtField);
48+
});
49+
50+
toggleExpiresAtField();
51+
});
52+
</script>

0 commit comments

Comments
 (0)