Skip to content

Commit

Permalink
Export tokens (#13)
Browse files Browse the repository at this point in the history
* create admin_engine

* add menu to admin

* add admin menu translation

* add code_groups_controller

* add admin template for view

* add table to index

* update private method gruops

* fix lint errors

* change datetime format in index table

* add action icons

* refactor index table body content

* fix index new_access_code_button

* add styles to icon_links

* add condition when migration is empty

* add card title to edit and new views

* update edit, form and new views

* add edit and new method to controller

* change action delete icon

* add command and create method

* add update command and update controller

* add destroy method

* refactor i18n variables

* configure action preview

* add pagination

* add command specs

* add controller spec

* test create, edit and update controller methods

* add system access_code_group_spec

* replace Date.today for Time.zone.today in spec

* update num of tockens in view

* add validations to codegroupform

* refactor create_code_group_spec

* save refactored command test

* add serializer

* refactor update_code_group_spec

* test edit case added to system spec

* fix ENV for codecov

* fix codecov

* lint

* use codecov token

* refactor create_code_group command

* refactor update_code_group_spec

* add checks to access_code_groups_spec

* refactor call command method

* add code_group_form_spec

* refactor index view

* use factory in code_groups_controller_spec

* add i18n key

* add validations checks

* adds a new access_code_group with nil expiration date

* add surveys helper_method

* add select to system test

* add permissions to controllers

* add export button

* add controller

* add back button

* add dropdown export

* add exporter

* Update config/locales/en.yml

* Update app/views/decidim/anonymous_codes/admin/codes/index.html.erb

* add pdf exporter

* add spec for exports

* fix spec

* fix permissions

---------

Co-authored-by: elviabth <[email protected]>
  • Loading branch information
microstudi and ElviaBth authored Apr 17, 2024
1 parent 0b8c09b commit 9bc4469
Show file tree
Hide file tree
Showing 27 changed files with 704 additions and 11 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

env:
RUBY_VERSION: 3.0.6
CI: 1
CI: "true"

jobs:
test-report:
Expand Down Expand Up @@ -62,8 +62,7 @@ jobs:
- name: Run RSpec
run: bundle exec rspec
env:
SIMPLECOV: 1
CODECOV: 1
SIMPLECOV: "true"

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Currently, the following ENV variables are supported:
| ------------ | ----------- |-------|
| ANONYMOUS_CODES_DEFAULT_TOKEN_LENGTH | The length of the codes generated if using the default token generator | `10` |
| ANONYMOUS_CODES_TOKEN_STYLE | The type of codes generated if using the default token generator (options are `alphanumeric`, `numeric`). (note that "numeric" still returns a string, just only formed by number chars) | `alphanumeric` |
| ANONYMOUS_CODES_EXPORT_FORMATS | Limit the formats for exporting (defaults to all available in Decidim). | `CSV JSON Excel FormPDF` |
It is also possible to configure the module using the `decidim-anonymous_codes` initializer:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module Decidim
module AnonymousCodes
module Admin
class ApplicationController < Decidim::Admin::ApplicationController
def permission_class_chain
[::Decidim::AnonymousCodes::Admin::Permissions] + super
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ def index
end

def new
enforce_permission_to :create, :anonymous_code_group

@form = form(CodeGroupForm).instance
end

def create
enforce_permission_to :create, :anonymous_code_group

@form = form(CodeGroupForm).from_params(params)

CreateCodeGroup.call(@form) do
Expand All @@ -33,10 +37,14 @@ def create
end

def edit
enforce_permission_to :update, :anonymous_code_group, code_group: code_group

@form = form(CodeGroupForm).from_model(code_group)
end

def update
enforce_permission_to :update, :anonymous_code_group, code_group: code_group

@form = form(CodeGroupForm).from_params(params)

UpdateCodeGroup.call(@form, code_group) do
Expand All @@ -53,6 +61,8 @@ def update
end

def destroy
enforce_permission_to :destroy, :anonymous_code_group, code_group: code_group

Decidim.traceability.perform_action!("delete", code_group, current_user) do
code_group.destroy!
end
Expand Down
77 changes: 77 additions & 0 deletions app/controllers/decidim/anonymous_codes/admin/codes_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

module Decidim
module AnonymousCodes
module Admin
class CodesController < ApplicationController
include Decidim::Admin::Paginable
include TranslatableAttributes

helper_method :tokens, :code_group, :export_jobs

def index
enforce_permission_to :view, :anonymous_code_token

@tokens = paginate(tokens.order(created_at: :desc))
end

def new
enforce_permission_to :create, :anonymous_code_token

# todo
end

def create
enforce_permission_to :create, :anonymous_code_token

# todo
end

def destroy
enforce_permission_to :destroy, :anonymous_code_token, token: token
# todo
end

def export
enforce_permission_to :export, :anonymous_code_token

if export_jobs[export_name].present?
Decidim.traceability.perform_action!("export_anonymous_codes_tokens", code_group, current_user, { name: export_name, format: export_format }) do
export_jobs[export_name].perform_later(current_user, code_group, export_format)
end

flash[:notice] = t("decidim.admin.exports.notice")
end

redirect_back(fallback_location: code_group_codes_path(code_group))
end

private

def tokens
AnonymousCodes::Token.for(code_group)
end

def token
@token ||= tokens.find(params[:id])
end

def code_group
@code_group ||= AnonymousCodes::Group.for(current_organization).find(params[:code_group_id])
end

def export_jobs
{ "all_tokens" => ExportGroupTokensJob }
end

def export_format
params[:format] || "json"
end

def export_name
params[:name] || "all_tokens"
end
end
end
end
end
15 changes: 15 additions & 0 deletions app/jobs/decidim/anonymous_codes/export_group_tokens_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Decidim
module AnonymousCodes
class ExportGroupTokensJob < ApplicationJob
queue_as :exports

def perform(user, group, format)
export_data = Decidim::Exporters.find_exporter(format).new(group.tokens, TokenSerializer).export

ExportMailer.export(user, "tokens_for_group_#{group.id}", export_data).deliver_now
end
end
end
end
12 changes: 10 additions & 2 deletions app/models/decidim/anonymous_codes/group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Group < ApplicationRecord

# prevent destroying if tokens have been used
before_destroy do
throw(:abort) if tokens.where(usage_count: 1..Float::INFINITY).count.positive?
throw(:abort) unless destroyable?
end

belongs_to :organization, class_name: "Decidim::Organization", foreign_key: "decidim_organization_id"
Expand All @@ -19,7 +19,15 @@ def self.for(organization)
end

def expired?
expires_at.present? && expires_at < Time.current
expires? && expires_at < Time.current
end

def expires?
expires_at.present?
end

def destroyable?
tokens.used.count.zero?
end
end
end
Expand Down
10 changes: 9 additions & 1 deletion app/models/decidim/anonymous_codes/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Token < ApplicationRecord

# prevent destroying if token has been used
before_destroy do
throw(:abort) if usage_count.positive?
throw(:abort) unless destroyable?
end

belongs_to :group, class_name: "Decidim::AnonymousCodes::Group", counter_cache: true
Expand All @@ -20,13 +20,21 @@ class Token < ApplicationRecord

scope :used, -> { where("usage_count > 0") }

def self.for(group)
where(group: group)
end

def available?
!used? && !expired? && active?
end

def used?
usage_count.to_i >= group.max_reuses.to_i
end

def destroyable?
!usage_count.positive?
end
end
end
end
51 changes: 51 additions & 0 deletions app/permissions/decidim/anonymous_codes/admin/permissions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module Decidim
module AnonymousCodes
module Admin
class Permissions < Decidim::DefaultPermissions
def permissions
return permission_action if permission_action.scope != :admin
return permission_action unless user&.admin?

anonymous_codes_group_action?
anonymous_codes_token_action?

permission_action
end

private

def anonymous_codes_group_action?
return unless permission_action.subject == :anonymous_code_group

case permission_action.action
when :create, :update, :export
allow!
when :destroy
toggle_allow(code_group.destroyable?) if code_group
end
end

def anonymous_codes_token_action?
return unless permission_action.subject == :anonymous_code_token

case permission_action.action
when :view, :create, :export
allow!
when :destroy
toggle_allow(token.destroyable?) if token
end
end

def code_group
context.fetch(:code_group, nil)
end

def token
context.fetch(:token, nil)
end
end
end
end
end
42 changes: 42 additions & 0 deletions app/serializers/decidim/anonymous_codes/token_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

module Decidim
module AnonymousCodes
# This class serializes a Token so can be exported to CSV, JSON or other formats.
class TokenSerializer < Decidim::Exporters::Serializer
include Decidim::TranslationsHelper

# Public: Initializes the serializer with a token.
def initialize(token)
@token = token
end

# Public: Exports a hash with the serialized data for this token.
def serialize
{
token: token.token,
resource_url: resource_url,
resource_type: token.group.resource_type,
resource_id: token.group.resource_id,
group: translated_attribute(token.group.title),
available: token.available?,
used: token.used?,
usage_count: token.usage_count,
expired: token.expired?,
expires_at: token.group.expires_at.present? ? I18n.l(token.group.expires_at, format: :decidim_short) : nil,
created_at: I18n.l(token.created_at, format: :decidim_short)
}
end

private

attr_reader :token

def resource_url
return nil unless token.group.resource

Decidim::ResourceLocatorPresenter.new(token.group.resource).url(token: token.token)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
<div class="card-divider">
<h2 class="card-title">
<%= t("code_groups.index.title", scope: "decidim.anonymous_codes.admin") %>
<a class="button tiny button--title new-access" href="<%= new_code_group_path %>"><%= t("code_groups.index.new_access_code_group_button", scope: "decidim.anonymous_codes.admin") %></a>
<% if allowed_to?(:create, :anonymous_code_group) %>
<a class="button tiny button--title new-access" href="<%= new_code_group_path %>"><%= t("code_groups.index.new_access_code_group_button", scope: "decidim.anonymous_codes.admin") %></a>
<% end %>
</h2>
</div>
<div class="card-section">
Expand All @@ -28,9 +30,10 @@
<td><%= "#{group.tokens.used.count} / #{group.tokens_count}" %></td>
<td><%= group.max_reuses %></td>
<td class="table-list__actions">
<%= icon_link_to "circle-x", code_group_path(group.id), t("actions.destroy", scope: "decidim.anonymous_codes.admin"), method: :delete, class: "action-icon--remove", data: { confirm: t("actions.confirm_destroy", scope: "decidim.anonymous_codes.admin") } %>
<%= icon_link_to "pencil", edit_code_group_path(group.id), t("actions.edit", scope: "decidim.anonymous_codes.admin"), class: "action-icon--edit" %>
<%= icon_link_to "eye", resource_path(group), t("actions.preview", scope: "decidim.anonymous_codes.admin"), class: "action-icon--preview#{group&.resource ? '':' invisible'}", target: :_blank %>
<%= icon_link_to "list", code_group_codes_path(group), t("actions.list_tokens", scope: "decidim.anonymous_codes.admin"), class: "action-icon--preview#{group&.resource ? '':' invisible'}" %>
<%= icon_link_to "pencil", edit_code_group_path(group.id), t("actions.edit", scope: "decidim.anonymous_codes.admin"), class: "action-icon--edit#{allowed_to?(:update, :anonymous_code_group, code_group: group) ? '' : ' invisible'}" %>
<%= icon_link_to "circle-x", code_group_path(group.id), t("actions.destroy", scope: "decidim.anonymous_codes.admin"), method: :delete, class: "action-icon--remove#{allowed_to?(:destroy, :anonymous_code_group, code_group: group) ? '' : ' invisible'}", data: { confirm: t("actions.confirm_destroy", scope: "decidim.anonymous_codes.admin") } %>
</td>
</tr>
<% end %>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<span class="exports dropdown tiny button button--simple" data-toggle="export-dropdown"><%= t "actions.export", scope: "decidim.admin" %></span>
<div class="dropdown-pane" id="export-dropdown" data-dropdown data-position=bottom data-alignment=right data-auto-focus="true" data-close-on-click="true">
<ul class="vertical menu add-components">
<% export_jobs.each do |name, _job| %>
<% Decidim::AnonymousCodes.export_formats.each do |format| %>
<li class="exports--format--<%= format.downcase %> exports--<%= name %>">
<%= link_to t("decidim.admin.exports.export_as", name: t("decidim.anonymous_codes.admin.exports.#{name}"), export_format: t("decidim.admin.exports.formats.#{format}")),
export_code_group_codes_path(code_group, format: format, name: name),
method: :post %>
</li>
<% end %>
<% end %>
</ul>
</div>
21 changes: 21 additions & 0 deletions app/views/decidim/anonymous_codes/admin/codes/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div class="card">
<div class="card-divider">
<h2 class="card-title flex--sbc">
<div>
<%= t("codes.index.title", scope: "decidim.anonymous_codes.admin", group: translated_attribute(code_group.title)) %>
</div>

<div class="flex--cc flex-gap--1">
<div id="js-other-actions-wrapper">
<%= render "export_dropdown" %>
</div>

<a class="button tiny button--title new-access" href="<%= code_groups_path %>"><%= t("codes.index.back", scope: "decidim.anonymous_codes.admin") %></a>
<a class="button tiny button--title new-access" href="<%= new_code_group_code_path(code_group) %>"><%= t("codes.index.new_codes_button", scope: "decidim.anonymous_codes.admin") %></a>
</div>
</h2>
</div>
<div class="card-section">
<%= tokens.all %>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<tr>
<td><%= token[:token] %></td>
<td><a href="<%= token[:resource_url] %>"><%= token[:resource_url] %></a></td>
<td><%= t("booleans.#{token[:available]}") %></td>
<td><%= t("booleans.#{token[:used]}") %></td>
<td><%= token[:usage_count] %></td>
</tr>
Loading

0 comments on commit 9bc4469

Please sign in to comment.