Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/feature/proposal-editing-time-…
Browse files Browse the repository at this point in the history
…hours-days' into feature/co-authorships-in-proposals
  • Loading branch information
antopalidi committed Jul 4, 2024
2 parents abde60f + 9ac2d78 commit d5ab0fd
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 14 deletions.
32 changes: 31 additions & 1 deletion decidim-admin/app/helpers/decidim/admin/settings_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ module SettingsHelper
select: :select_field,
scope: :scope_field,
enum: :collection_radio_buttons,
time: :datetime_field
time: :datetime_field,
integer_with_units: :integer_with_units
}.freeze

# Renders a form field that matches a settings attribute's type.
Expand Down Expand Up @@ -58,6 +59,8 @@ def settings_attribute_input(form, attribute, name, i18n_scope, options = {})
render_select_form_field(form, attribute, name, i18n_scope, options)
elsif form_method == :scope_field
scopes_select_field(form, name)
elsif form_method == :integer_with_units
integer_with_units(form, attribute, name, i18n_scope, options)
else
form.send(form_method, name, options)
end
Expand Down Expand Up @@ -165,6 +168,33 @@ def build_enum_choices(name, i18n_scope, choices)
[t("#{name}_choices.#{choice}", scope: i18n_scope), choice]
end
end

# Renders a form field that includes an integer input and a select dropdown for units.
#
# @param form (see #settings_attribute_input)
# @param attribute [Decidim::SettingsManifest::Attribute] The attribute to be rendered
# @param name (see #settings_attribute_input)
# @param i18n_scope (see #settings_attribute_input)
# @param options (see #settings_attribute_input)
# @option options [String] :label The label text for the field
# @return [ActiveSupport::SafeBuffer] Rendered form field
def integer_with_units(form, attribute, name, i18n_scope, options)
value = form.object.send(name)

number_value = value[0].to_i
unit_value = value[1].to_s

number_field_html = form.number_field(name, options.merge(label: false,
value: number_value,
name: "#{form.field_name(name)}[0]",
style: "flex: 0 0 25%;"))
select_field_html = form.select(name,
attribute.build_units.map { |unit| [t("#{name}_units.#{unit}", scope: i18n_scope), unit] },
{ label: false, value: unit_value },
{ name: "#{form.field_name(name)}[1]", style: "flex: 1 1 75%;" })

content_tag(:label, options[:label]) + content_tag(:div, number_field_html + select_field_html, class: "flex space-x-2 items-center")
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
$(() => {
const $limitedTimeLabel = $("label[for='component_settings_proposal_edit_time_limited']")
const $limitedTimeRadioButton = $("#component_settings_proposal_edit_time_limited")
const $infiniteTimeRadioButton = $("#component_settings_proposal_edit_time_infinite")
const $editTimeContainer = $(".proposal_edit_before_minutes_container")

$editTimeContainer.detach().appendTo($limitedTimeLabel)
const $limitedTimeRadioButton = $("#component_settings_proposal_edit_time_limited");
const $infiniteTimeRadioButton = $("#component_settings_proposal_edit_time_infinite");
const $editTimeContainer = $(".edit_time_container");

if ($infiniteTimeRadioButton.is(":checked")) {
$editTimeContainer.hide();
Expand Down
2 changes: 2 additions & 0 deletions decidim-core/lib/decidim/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Attributes
autoload :Model, "decidim/attributes/model"
autoload :Symbol, "decidim/attributes/symbol"
autoload :Integer, "decidim/attributes/integer"
autoload :IntegerWithUnits, "decidim/attributes/integer_with_units"

# Base types
ActiveModel::Type.register(:array, Decidim::Attributes::Array)
Expand All @@ -28,6 +29,7 @@ module Attributes
ActiveModel::Type.register(:"decidim/attributes/localized_date", Decidim::Attributes::LocalizedDate)
ActiveModel::Type.register(:"decidim/attributes/clean_string", Decidim::Attributes::CleanString)
ActiveModel::Type.register(:"decidim/attributes/blob", Decidim::Attributes::Blob)
ActiveModel::Type.register(:"decidim/attributes/integer_with_units", Decidim::Attributes::IntegerWithUnits)

ActiveModel::Type.register(:integer, Decidim::Attributes::Integer)
end
Expand Down
27 changes: 27 additions & 0 deletions decidim-core/lib/decidim/attributes/integer_with_units.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Decidim
module Attributes
# Custom attributes value to represent an Integer with units.
class IntegerWithUnits < ActiveModel::Type::Value
def type
:"decidim/attributes/integer_with_units"
end

def cast(value)
return nil if value.nil?

case value
when ::Hash
[value["0"].to_i.abs, value["1"].to_s]
when ::Array
return value if value.size != 2

[value[0].to_i.abs, value[1].to_s]
else
value
end
end
end
end
end
33 changes: 33 additions & 0 deletions decidim-core/lib/decidim/settings_manifest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,29 @@ def manifest
validates name, presence: true if attribute.required
validates name, inclusion: { in: attribute.build_choices } if attribute.type == :enum
end

SettingsManifest.add_integer_with_units_validation(self, name, attribute) if attribute.type == :integer_with_units
end
end

@schema.manifest = self
@schema
end

def self.add_integer_with_units_validation(schema, name, attribute)
schema.class_eval do
validate do
value = send(name)
value = [value["0"].to_i, value["1"].to_s] if value.is_a?(::Hash)

errors.add(name, :invalid) unless value.is_a?(::Array) &&
value.size == 2 &&
value[0].is_a?(Integer) &&
attribute.build_units.include?(value[1])
end
end
end

def required_attributes_for_authorization
attributes.select { |_, attribute| attribute.required_for_authorization? }
end
Expand All @@ -91,6 +107,7 @@ class Attribute
TYPES = {
boolean: { klass: Boolean, default: false },
integer: { klass: Integer, default: 0 },
integer_with_units: { klass: Decidim::Attributes::IntegerWithUnits, default: [5, "minutes"] },
string: { klass: String, default: nil },
float: { klass: Float, default: nil },
text: { klass: String, default: nil },
Expand All @@ -111,9 +128,21 @@ class Attribute
attribute :required_for_authorization, Boolean, default: false
attribute :readonly
attribute :choices
attribute :units
attribute :item_classes, Array, default: []
attribute :include_blank, Boolean, default: false

validates :type, inclusion: { in: TYPES.keys }
validate :validate_integer_with_units_structure

def validate_integer_with_units_structure
return unless type == :integer_with_units

errors.add(:default, :invalid) unless default_value.is_a?(::Array) &&
default_value.size == 2 &&
default_value[0].is_a?(Integer) &&
build_units.include?(default_value[1])
end

def type_class
TYPES[type][:klass]
Expand All @@ -127,6 +156,10 @@ def build_choices
choices.try(:call) || choices
end

def build_units
units.try(:call) || units
end

def readonly?(context)
readonly&.call(context)
end
Expand Down
69 changes: 69 additions & 0 deletions decidim-core/spec/lib/attributes/integer_with_units_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

require "spec_helper"

module Decidim
describe Attributes::IntegerWithUnits do
describe "#cast" do
subject { described_class.new.cast(value) }

describe "#cast" do
context "when given nil" do
let(:value) { nil }

it "returns nil" do
expect(subject).to be_nil
end
end

context "when given a Hash" do
let(:value) { { "0" => "5", "1" => "minutes" } }

it "returns an array with the integer and the unit" do
expect(subject).to eq([5, "minutes"])
end

context "when the integer is negative" do
let(:value) { { "0" => "-5", "1" => "minutes" } }

it "returns an array with the absolute value of the integer and the unit" do
expect(subject).to eq([5, "minutes"])
end
end
end

context "when given an Array" do
let(:value) { %w(5 minutes) }

it "returns an array with the integer and the unit" do
expect(subject).to eq([5, "minutes"])
end

context "when the array size is not 2" do
let(:value) { ["5"] }

it "returns the original value" do
expect(subject).to eq(["5"])
end
end

context "when the integer is negative" do
let(:value) { %w(-5 minutes) }

it "returns an array with the absolute value of the integer and the unit" do
expect(subject).to eq([5, "minutes"])
end
end
end

context "when given other values" do
let(:value) { "some string" }

it "returns the original value" do
expect(subject).to eq("some string")
end
end
end
end
end
end
14 changes: 12 additions & 2 deletions decidim-proposals/app/models/decidim/proposals/proposal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,18 @@ def within_edit_time_limit?
return true if draft?
return true if component.settings.proposal_edit_time == "infinite"

limit = updated_at + component.settings.proposal_edit_before_minutes.minutes
Time.current < limit
time_value, time_unit = component.settings.edit_time

limit_time = case time_unit
when "minutes"
updated_at + time_value.minutes
when "hours"
updated_at + time_value.hours
else
updated_at + time_value.days
end

Time.current < limit_time
end

def process_amendment_state_change!
Expand Down
10 changes: 9 additions & 1 deletion decidim-proposals/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ en:
random: Random
recent: Recent
with_more_authors: With more authors
edit_time: Proposals can be edited by authors before this much time passes
edit_time_units:
days: Days
hours: Hours
minutes: Minutes
geocoding_enabled: Geocoding enabled
minimum_votes_per_user: Minimum votes per user
new_proposal_body_template: New proposal body template
Expand All @@ -180,11 +185,14 @@ en:
participatory_texts_enabled: Participatory texts enabled
participatory_texts_enabled_readonly: Cannot interact with this setting if there are existing proposals. Please, create a new `Proposals component` if you want to enable this feature or discard all imported proposals in the `Participatory Texts` menu if you want to disable it.
proposal_answering_enabled: Proposal answering enabled
proposal_edit_before_minutes: Proposals can be edited by authors before this many minutes passes
proposal_edit_time: Proposal editing
proposal_edit_time_choices:
infinite: Allow editing proposals for an infinite amount of time
limited: Allow editing of proposals within a specific timeframe
proposal_edit_time_unit_options:
days: Days
hours: Hours
minutes: Minutes
proposal_length: Maximum proposal body length
proposal_limit: Proposal limit per participant
proposal_wizard_step_1_help_text: Proposal wizard "Create" step help text
Expand Down
4 changes: 2 additions & 2 deletions decidim-proposals/lib/decidim/proposals/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
settings.attribute :minimum_votes_per_user, type: :integer, default: 0, required: true
settings.attribute :proposal_limit, type: :integer, default: 0, required: true
settings.attribute :proposal_length, type: :integer, default: 500
settings.attribute :proposal_edit_time, type: :enum, default: "limited", choices: -> { %w(limited infinite) }
settings.attribute :proposal_edit_before_minutes, type: :integer, default: 5, required: true
settings.attribute :proposal_edit_time, type: :enum, default: "limited", choices: -> { %w(infinite limited) }
settings.attribute :edit_time, type: :integer_with_units, default: [5, "minutes"], required: true, units: %w(minutes hours days)
settings.attribute :threshold_per_proposal, type: :integer, default: 0, required: true
settings.attribute :can_accumulate_votes_beyond_threshold, type: :boolean, default: false
settings.attribute :proposal_answering_enabled, type: :boolean, default: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@
it_behaves_like "has mandatory config setting", :minimum_votes_per_user
end

context "when proposal_edit_before_minutes is empty" do
it_behaves_like "has mandatory config setting", :proposal_edit_before_minutes
context "when proposal_edit_time is empty" do
it_behaves_like "has mandatory config setting", :edit_time
end

context "when comments_max_length is empty" do
Expand Down
Loading

0 comments on commit d5ab0fd

Please sign in to comment.