Skip to content

Commit

Permalink
Improve components and spaces sharing with administrable tokens (deci…
Browse files Browse the repository at this point in the history
…dim#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 <[email protected]>
  • Loading branch information
microstudi and ElviaBth authored Oct 16, 2024
1 parent e09045e commit a74e5e2
Show file tree
Hide file tree
Showing 93 changed files with 2,005 additions and 302 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

require "spec_helper"

describe "Preview accountability with share token" do
describe "preview accountability with a share token" do
let(:manifest_name) { "accountability" }

include_context "with a component"
it_behaves_like "preview component with share_token"
it_behaves_like "preview component with a share_token"
end
39 changes: 39 additions & 0 deletions decidim-admin/app/commands/decidim/admin/create_share_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Decidim
module Admin
# A command with all the business logic to create a share token.
# This command is called from the controller.
class CreateShareToken < Decidim::Commands::CreateResource
fetch_form_attributes :token, :expires_at, :registered_only, :organization, :user, :token_for

protected

def resource_class = Decidim::ShareToken

def extra_params
{
participatory_space: {
title: participatory_space&.title
},
resource: {
title: component&.name
}
}
end

def participatory_space
return form.token_for if form.token_for.try(:manifest).is_a?(Decidim::ParticipatorySpaceManifest)
return current_participatory_space if respond_to?(:current_participatory_space)

component&.participatory_space
end

def component
return form.token_for if form.token_for.is_a?(Decidim::Component)

form.token_for.try(:component)
end
end
end
end
22 changes: 22 additions & 0 deletions decidim-admin/app/commands/decidim/admin/destroy_share_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Decidim
module Admin
# A command with all the business logic to destroy a share token.
# This command is called from the controller.
class DestroyShareToken < Decidim::Commands::DestroyResource
delegate :participatory_space, :component, to: :resource

def extra_params
{
participatory_space: {
title: participatory_space&.title
},
resource: {
title: component&.name
}
}
end
end
end
end
24 changes: 24 additions & 0 deletions decidim-admin/app/commands/decidim/admin/update_share_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Decidim
module Admin
# A command with all the business logic to update a share token.
# This command is called from the controller.
class UpdateShareToken < Decidim::Commands::UpdateResource
fetch_form_attributes :expires_at, :registered_only

delegate :participatory_space, :component, to: :resource

def extra_params
{
participatory_space: {
title: participatory_space&.title
},
resource: {
title: component&.name
}
}
end
end
end
end
116 changes: 109 additions & 7 deletions decidim-admin/app/controllers/decidim/admin/share_tokens_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,66 @@

module Decidim
module Admin
# This is an abstract controller allows sharing unpublished things.
# Final implementation must inherit from this controller and implement the `resource` method.
class ShareTokensController < Decidim::Admin::ApplicationController
include Decidim::Admin::Filterable

helper_method :current_token, :resource, :resource_title, :share_tokens_path

def index
enforce_permission_to :read, :share_tokens
@share_tokens = filtered_collection
end

def new
enforce_permission_to :create, :share_tokens
@form = form(ShareTokenForm).instance
end

def create
enforce_permission_to :create, :share_tokens
@form = form(ShareTokenForm).from_params(params, resource:)

CreateShareToken.call(@form) do
on(:ok) do
flash[:notice] = I18n.t("share_tokens.create.success", scope: "decidim.admin")
redirect_to share_tokens_path
end

on(:invalid) do
flash.now[:alert] = I18n.t("share_tokens.create.invalid", scope: "decidim.admin")
render action: "new"
end
end
end

def edit
enforce_permission_to(:update, :share_tokens, share_token: current_token)
@form = form(ShareTokenForm).from_model(current_token)
end

def update
enforce_permission_to(:update, :share_tokens, share_token: current_token)
@form = form(ShareTokenForm).from_params(params, resource:)

UpdateShareToken.call(@form, current_token) do
on(:ok) do
flash[:notice] = I18n.t("share_tokens.update.success", scope: "decidim.admin")
redirect_to share_tokens_path
end

on(:invalid) do
flash.now[:alert] = I18n.t("share_tokens.update.error", scope: "decidim.admin")
render :edit
end
end
end

def destroy
enforce_permission_to(:destroy, :share_token, share_token:)
enforce_permission_to(:destroy, :share_tokens, share_token: current_token)

Decidim::Commands::DestroyResource.call(share_token, current_user) do
DestroyShareToken.call(current_token, current_user) do
on(:ok) do
flash[:notice] = I18n.t("share_tokens.destroy.success", scope: "decidim.admin")
end
Expand All @@ -15,15 +70,62 @@ def destroy
end
end

redirect_back(fallback_location: root_path)
redirect_to share_tokens_path
end

private

def share_token
@share_token ||= Decidim::ShareToken.where(
organization: current_organization
).find(params[:id])
# override this method in the destination controller to specify the resource associated with the shared token (ie: a component)
def resource
raise NotImplementedError
end

# Override also this method if resource does not respond to a translatable name or title
def resource_title
translated_attribute(resource.try(:name) || resource.title)
end

# sets the prefix for the route helper methods (this may vary depending on the resource type)
# This setup works fine for participatory spaces and components, override if needed
def route_name
@route_name ||= "#{resource.manifest.route_name}_"
end

def route_proxy
@route_proxy ||= EngineRouter.admin_proxy(resource.try(:participatory_space) || resource)
end

# returns the proper path for managing a share token according to the resource
# this works fine for components and participatory spaces, override if needed
def share_tokens_path(method = :index, options = {})
args = resource.is_a?(Decidim::Component) ? [resource, options] : [options]

case method
when :index, :create
route_proxy.send("#{route_name}share_tokens_path", *args)
when :new
route_proxy.send("new_#{route_name}share_token_path", *args)
when :update, :destroy
route_proxy.send("#{route_name}share_token_path", *args)
when :edit
route_proxy.send("edit_#{route_name}share_token_path", *args)
end
end

def base_query
collection
end

def collection
@collection ||= Decidim::ShareToken.where(organization: current_organization, token_for: resource)
end

def filters
[]
end

def current_token
@current_token ||= collection.find(params[:id])
end
end
end
Expand Down
55 changes: 55 additions & 0 deletions decidim-admin/app/forms/decidim/admin/share_token_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module Decidim
module Admin
class ShareTokenForm < Decidim::Form
mimic :share_token

attribute :token, String
attribute :automatic_token, Boolean, default: true
attribute :expires_at, Decidim::Attributes::TimeWithZone
attribute :no_expiration, Boolean, default: true
attribute :registered_only, Boolean, default: false

validates :token, presence: true, if: ->(form) { form.automatic_token.blank? }
validate :token_uniqueness, if: ->(form) { form.automatic_token.blank? }

validates_format_of :token, with: /\A[a-zA-Z0-9_-]+\z/, allow_blank: true
validates :expires_at, presence: true, if: ->(form) { form.no_expiration.blank? }

def map_model(model)
self.no_expiration = model.expires_at.blank?
end

def token
super.strip.upcase.gsub(/\s+/, "-") if super.present?
end

def expires_at
return nil if no_expiration.present?

super
end

def token_for
context[:resource]
end

def organization
context[:current_organization]
end

def user
context[:current_user]
end

private

def token_uniqueness
return unless Decidim::ShareToken.where(organization:, token_for:, token:).where.not(id:).any?

errors.add(:token, :taken)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
<span class="action-space icon"></span>
<% end %>

<% if allowed_to? :share, :component, component: component %>
<%= 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" %>
<% if component.manifest.admin_engine && allowed_to?(:share, :component, component: component) %>
<%= icon_link_to "share-line", component_share_tokens_path(component_id: component), t("actions.share_tokens", scope: "decidim.admin"), class: "action-icon--share" %>
<% else %>
<span class="action-space icon"></span>
<% end %>

<% if allowed_to? :update, :component, component: component %>
<%= icon_link_to "settings-4-line", url_for(action: :edit, id: component, controller: "components"), t("actions.configure", scope: "decidim.admin"), class: "action-icon--configure" %>
<% else %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,4 @@
</div>
</div>
<% end %>
<% if component && component.persisted? && !component.published? %>
<div class="card" data-component="accordion" id="accordion-share_tokens">
<div id="panel-share_tokens" class="card-section">
<div class="row column">
<%= render partial: "decidim/admin/share_tokens/share_tokens", locals: { share_tokens: form.object.share_tokens } %>
</div>
</div>
</div>
<% end %>
</div>
52 changes: 52 additions & 0 deletions decidim-admin/app/views/decidim/admin/share_tokens/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<div class="row column">
<label><%= t("share_tokens.form.expires_at", scope: "decidim.admin") %></label>
<%= 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| %>
<div>
<%= b.radio_button %>
<%= b.label %>
</div>
<% end %>
<div id="expires_at_field_wrapper" class="hidden mt-4 row column">
<%= form.datetime_field :expires_at, label: t("share_tokens.form.custom_expiration", scope: "decidim.admin") %>
</div>
</div>

<div class="row column">
<label><%= t("share_tokens.form.registered_only", scope: "decidim.admin") %></label>
<%= form.collection_radio_buttons :registered_only, [
[t("share_tokens.form.true", scope: "decidim.admin"), true],
[t("share_tokens.form.false", scope: "decidim.admin"), false]
], :last, :first do |b| %>
<div>
<%= b.label do %>
<%= b.radio_button %>
<%= b.text %>
<% end %>
</div>
<% end %>
</div>

<script>
document.addEventListener("DOMContentLoaded", () => {
const expiresButton = document.querySelector("input[name='share_token[no_expiration]'][value='false']");
const expiresAtRadioButtons = document.querySelectorAll("input[name='share_token[no_expiration]']");
const expiresAtWrapper = document.getElementById("expires_at_field_wrapper");
const expiresAtInput = document.querySelector("input[name='share_token[expires_at]']");

const toggleExpiresAtField = () => {
if (expiresButton.checked) {
expiresAtWrapper.classList.remove("hidden");
} else {
expiresAtWrapper.classList.add("hidden");
expiresAtInput.value = "";
expiresAtInput.removeAttribute("required");
}
};

expiresAtRadioButtons.forEach(expiresAtRadioButton => {
expiresAtRadioButton.addEventListener("change", toggleExpiresAtField);
});

toggleExpiresAtField();
});
</script>
Loading

0 comments on commit a74e5e2

Please sign in to comment.