Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share unpublished components with administrable tokens #77

Draft
wants to merge 54 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9b59461
remove share_tokens from admin/components/_form
ElviaBth Jul 10, 2024
1836d27
add routes
ElviaBth Jul 10, 2024
addaee9
add component name on index title
ElviaBth Jul 10, 2024
0ec9895
add form and new views
ElviaBth Jul 11, 2024
c9284ee
fix index styles
ElviaBth Jul 11, 2024
81fe2c1
refactor share_token form
ElviaBth Jul 11, 2024
e9f0bc4
refactor share_token form
ElviaBth Jul 11, 2024
330ebbf
add locales keys
ElviaBth Jul 12, 2024
d1c7a49
add automatic_token attribute
ElviaBth Jul 12, 2024
02f2384
refactor createsharetoken command
ElviaBth Jul 12, 2024
de4278a
remove target_blank
ElviaBth Jul 15, 2024
ea6051d
add more methods to sharetokenform
ElviaBth Jul 15, 2024
d3e8266
refactor token_for definition
ElviaBth Jul 15, 2024
0ac71f5
add edit and update method to controller
ElviaBth Jul 15, 2024
f3e01b5
fix routing error
ElviaBth Jul 15, 2024
2017e8d
add create and update command specs
ElviaBth Jul 16, 2024
7465d82
fix spelling error
ElviaBth Jul 16, 2024
832130b
fix spelling error
ElviaBth Jul 16, 2024
d0373bd
add share_token_form spec
ElviaBth Jul 16, 2024
ab4bace
remove target_ blank from edit action
ElviaBth Jul 16, 2024
b657bc0
remove set_default_expiration method
ElviaBth Jul 16, 2024
1432f3e
add collection_radio_buttonts
ElviaBth Jul 16, 2024
bf0dc07
fix no expitration bug
ElviaBth Jul 17, 2024
2ed3396
add registered_only to decidim_share_tokens migration
ElviaBth Jul 17, 2024
164cd3b
add more checks to share_tokens_form spec
ElviaBth Jul 17, 2024
d9a3e3d
add more checks to commands create and update share_token spec
ElviaBth Jul 17, 2024
eb111d5
add copy clipboard functionality
ElviaBth Jul 17, 2024
4cc6945
fix lint errors
ElviaBth Jul 17, 2024
a972b59
add is_active_link for share_tokens_path
ElviaBth Jul 17, 2024
3335996
remove space detected
ElviaBth Jul 17, 2024
bfb6894
fix noMethodError on user_can_preview_component? method
ElviaBth Jul 17, 2024
bdfbca5
add enforce_permission to controller
ElviaBth Jul 18, 2024
5d626d4
fix manage_components_share_tokens spec
ElviaBth Jul 18, 2024
ca02be2
fix manage_process_components_examples spec
ElviaBth Jul 18, 2024
6eec8a1
fix share_token_spec
ElviaBth Jul 18, 2024
7698f3e
add share_tokens routes on initiatives-module
ElviaBth Jul 18, 2024
855df0b
remove unused i18n keys
ElviaBth Jul 19, 2024
760d51a
add more checks to manage_component_share_tokens
ElviaBth Jul 19, 2024
b8695c4
fix has to edit a share token case check
ElviaBth Jul 19, 2024
f116064
add spec check allows copying the share link from the share token
ElviaBth Jul 19, 2024
f557826
save clipboard-copy-label-original
ElviaBth Jul 19, 2024
6b6339b
Merge branch 'develop' into feature/share_unpublish_components_with_t…
microstudi Jul 23, 2024
a7df6af
Merge branch 'develop' into feature/share_unpublish_components_with_t…
microstudi Jul 24, 2024
7e69e24
fix clipboard js
microstudi Jul 24, 2024
61e7497
fix validations and views
microstudi Jul 25, 2024
092d408
add specs
microstudi Jul 25, 2024
a9080ab
update permissions
microstudi Jul 25, 2024
8d5320a
update documentation
microstudi Jul 25, 2024
f0d31ea
add help text
microstudi Jul 26, 2024
b2daddd
allow to manage participatory spaces share tokens
microstudi Jul 26, 2024
8e6a317
add space specs
microstudi Jul 26, 2024
0e50826
add preview specs
microstudi Jul 26, 2024
6b1dddb
fix clipboard
microstudi Jul 27, 2024
b657fb5
fix specs
microstudi Jul 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
15 changes: 15 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,15 @@
# frozen_string_literal: true

module Decidim
module Admin
# A command with all the business logic to create a taxonomy.
# 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
end
end
end
11 changes: 11 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,11 @@
# frozen_string_literal: true

module Decidim
module Admin
# A command with all the business logic to update a taxonomy.
# This command is called from the controller.
class UpdateShareToken < Decidim::Commands::UpdateResource
fetch_form_attributes :expires_at, :registered_only
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
Decidim::Commands::DestroyResource.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", 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 @@ -114,13 +114,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>

This file was deleted.

32 changes: 32 additions & 0 deletions decidim-admin/app/views/decidim/admin/share_tokens/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<% add_decidim_page_title(t(".title")) %>
<div class="item_show__header">
<h1 class="item_show__header-title">
<%= t ".title", name: resource_title %>
<a class="button button__sm button__secondary" href="<%= share_tokens_path %>"><%= t("share_tokens.index.back_to_share_tokens", scope: "decidim.admin") %></a>
</h1>
</div>
<div class="item__edit-form">
<%= decidim_form_for(@form, url: share_tokens_path(:update, id: current_token), html: { class: "form-defaults form edit_share_token" }) do |f| %>
<div class="card">
<div class="card-section">
<div class="form__wrapper">
<div class="card pt-4">
<div class="row column">
<label for="share_token-token"><%= t("token", scope: "decidim.admin.models.share_token.fields") %></label>
<div class="flex gap-4">
<%= text_field_tag :token, current_token.token, id: "share_token-token", aria: { label: t("token", scope: "decidim.admin.models.share_token.fields") }, disabled: true %>
<button type="button" class="button button__sm button__secondary text-nowrap" data-clipboard-copy="#share_token-token" data-clipboard-content="<%= current_token.url %>" data-clipboard-copy-label="<%= t("copied", scope: "decidim.admin.share_tokens.index") %>" data-clipboard-copy-message="<%= t("copy_message", scope: "decidim.admin.share_tokens.index") %>"><%= t("actions.copy_link", scope: "decidim.admin.share_tokens") %></button>
</div>
</div>
<%= render partial: "form", object: f %>
</div>
</div>
</div>
</div>
<div class="item__edit-sticky">
<div class="item__edit-sticky-container">
<%= f.submit t(".update"), class: "button button__sm button__secondary" %>
</div>
</div>
<% end %>
</div>
Loading
Loading