diff --git a/app/controllers/consultations_controller.rb b/app/controllers/consultations_controller.rb index 0c40010..1f4241f 100644 --- a/app/controllers/consultations_controller.rb +++ b/app/controllers/consultations_controller.rb @@ -73,7 +73,7 @@ def create_params end def update_params - params.require(:consultation).permit(:title, :description, :status, config: %i[mode ballot]) + params.require(:consultation).permit(:title, :description, :status, config: %i[mode ballot distribution]) end def event diff --git a/app/interactors/cast_votes.rb b/app/interactors/cast_votes.rb index b22396c..aee616c 100644 --- a/app/interactors/cast_votes.rb +++ b/app/interactors/cast_votes.rb @@ -64,13 +64,9 @@ def cast_vote(vote_params) question = vote_params[:question] envelope = Envelope.new(question, current_user, **vote_params.to_h.symbolize_keys) - envelope.save(async: async?) + envelope.save_later envelope.receipt.save! self.receipts << envelope.receipt end - - def async? - Rails.configuration.x.asembleo.async_vote - end end diff --git a/app/interactors/create_magic_link.rb b/app/interactors/create_magic_link.rb index aaed1fd..adc365f 100644 --- a/app/interactors/create_magic_link.rb +++ b/app/interactors/create_magic_link.rb @@ -45,6 +45,6 @@ def user end def deliver - SessionsMailer.magic_link_email(email, token).deliver_later + SessionsMailer.magic_link_email(email, token.class.name, token.id, token.to_hash, scope: :global).deliver_later end end diff --git a/app/interactors/create_token.rb b/app/interactors/create_token.rb index 9de0b74..e3a79ab 100644 --- a/app/interactors/create_token.rb +++ b/app/interactors/create_token.rb @@ -87,6 +87,7 @@ def issue_receipt end def deliver - SessionsMailer.magic_link_email(identifier, token.reload).deliver_later + token.reload + SessionsMailer.magic_link_email(identifier, token.class.name, token.id, token.to_hash, scope:).deliver_later end end diff --git a/app/interactors/redirect_by_session.rb b/app/interactors/redirect_by_session.rb index f33e1dc..6551926 100644 --- a/app/interactors/redirect_by_session.rb +++ b/app/interactors/redirect_by_session.rb @@ -32,7 +32,8 @@ def active_question join_condition = [ receipts[:question_id].eq(questions[:question_id]), - receipts[:token_id].eq(identity.id) + receipts[:voter_type].eq(identity.class.name), + receipts[:voter_id].eq(identity.id) ] where = { status: :opened, diff --git a/app/jobs/vote_cast_job.rb b/app/jobs/vote_cast_job.rb index 27f89b3..c9760af 100644 --- a/app/jobs/vote_cast_job.rb +++ b/app/jobs/vote_cast_job.rb @@ -4,11 +4,13 @@ class VoteCastJob < ApplicationJob queue_as :default # TODO: Vote hash chaining - def perform(question_id, token_id, value, created_at) + def perform(question_id, token_class, token_id, value, created_at) question = Question.find(question_id) - token = Token.find(token_id) + + token_class = token_class.constantize + token = token_class.find(token_id) envelope = Envelope.new(question, token, value:, created_at:) - envelope.save + envelope.save! end end diff --git a/app/mailers/sessions_mailer.rb b/app/mailers/sessions_mailer.rb index 42b7dfe..95b6d4d 100644 --- a/app/mailers/sessions_mailer.rb +++ b/app/mailers/sessions_mailer.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true class SessionsMailer < ApplicationMailer - def magic_link_email(to, token) - @token = token + def magic_link_email(to, voter_type, voter_id, voter_hash, scope: :consultation) + @scope = scope.to_sym + @voter_hash = voter_hash raise Errors::InvalidEmail unless EmailValidator.valid?(to, mode: :strict) tag = TokenTag.sent @@ -14,20 +15,22 @@ def magic_link_email(to, token) raise e ensure - token.tags << tag - token.save! + return unless voter_type == 'Token' + + tag.token_id = voter_id + tag.save! end private def template_name - return 'user_magic_link_email' if @token.scope == 'global' + return 'user_magic_link_email' if @scope == :global 'magic_link_email' end def subject - return I18n.t('sessions_mailer.user_magic_link_email.subject') if @token.scope == 'global' + return I18n.t('sessions_mailer.user_magic_link_email.subject') if @scope == :global I18n.t('sessions_mailer.magic_link_email.subject') end diff --git a/app/models/envelope.rb b/app/models/envelope.rb index 727f19a..2a04f89 100644 --- a/app/models/envelope.rb +++ b/app/models/envelope.rb @@ -34,24 +34,25 @@ def receipt return @receipt if defined?(@receipt) @receipt ||= Receipt.new.tap do |r| - r.token_id = token.id + r.voter = token r.question = question r.created_at = created_at r.fingerprint = FingerprintService.generate(r, token.to_hash, values) end end - def save(async: false) - if async - VoteCastJob.perform_later(question.id, token.id, values, receipt.created_at) - else - votes.map(&:save!) - end + def save_later + VoteCastJob.perform_later(question.id, token.class.name, token.id, values, receipt.created_at) + end + + def save! + votes.each(&:save!) end private def vote_alias + return unless token.is_a?(Token) return unless consultation.ballot == 'open' token.on_behalf_of || token.alias || token.to_hash diff --git a/app/models/question.rb b/app/models/question.rb index 70d43ce..5f5ac2a 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -19,8 +19,8 @@ def valid_option?(value) options.exists?(value:) end - def voted?(token) - receipts.exists?(token:) + def voted?(voter) + receipts.exists?(voter:) end def short_description diff --git a/app/models/receipt.rb b/app/models/receipt.rb index 0294ffd..6c66b7b 100644 --- a/app/models/receipt.rb +++ b/app/models/receipt.rb @@ -3,6 +3,6 @@ class Receipt < ApplicationRecord has_paper_trail - belongs_to :token + belongs_to :voter, polymorphic: true belongs_to :question end diff --git a/app/models/token.rb b/app/models/token.rb index 3222f6f..eac1b98 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -8,7 +8,7 @@ class Token < ApplicationRecord belongs_to :consultation, optional: true belongs_to :event, optional: true - has_many :receipts, dependent: :destroy + has_many :receipts, dependent: :destroy, as: :voter has_many :tags, foreign_key: 'token_id', class_name: 'TokenTag', dependent: :destroy, inverse_of: :token enum role: { voter: 0, manager: 1, admin: 2 } diff --git a/app/models/user.rb b/app/models/user.rb index 55d32b5..a2624ad 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,7 +5,7 @@ class User < ApplicationRecord has_paper_trail - has_many :receipts, foreign_key: 'token_id', dependent: :destroy + has_many :receipts, dependent: :destroy, as: :voter enum :role, { voter: 0, manager: 1, admin: 2 }, default: :voter enum :status, { enabled: 1, disabled: 0 }, default: :disabled diff --git a/app/services/markdown_service.rb b/app/services/markdown_service.rb index 03c32c8..b3621f9 100644 --- a/app/services/markdown_service.rb +++ b/app/services/markdown_service.rb @@ -20,7 +20,10 @@ def self.render(text, plain_text: false) ActionController::Base.helpers.sanitize(content) end - def header(text, _header_level) - %(#{text}) + def header(text, header_level) + new_header_level = header_level + 2 + new_header_level = 6 if new_header_level > 6 + + %(#{text}) end end diff --git a/app/views/consultations/show.html.erb b/app/views/consultations/show.html.erb index 55a40d5..e3ece4d 100644 --- a/app/views/consultations/show.html.erb +++ b/app/views/consultations/show.html.erb @@ -1,5 +1,5 @@

<%= @consultation.title %>

+<%= render @consultation.status %>
<%= markdown @consultation.description %> -
-<%= render @consultation.status %> \ No newline at end of file + \ No newline at end of file diff --git a/app/views/sessions_mailer/magic_link_email.text.erb b/app/views/sessions_mailer/magic_link_email.text.erb index 56310f6..428c8fe 100644 --- a/app/views/sessions_mailer/magic_link_email.text.erb +++ b/app/views/sessions_mailer/magic_link_email.text.erb @@ -1,8 +1,8 @@ <%= t('application_mailer.greeting') %>, <%= t('sessions_mailer.magic_link_email.body', - magic_link_url: magic_link_url(@token.to_hash), + magic_link_url: magic_link_url(@voter_hash), root_url: root_url, - token: @token).strip %> + token: @voter_hash).strip %> <%= t('application_mailer.complimentary_close', recipient: Rails.configuration.x.asembleo.title).strip %> \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 193eb85..b235c84 100644 --- a/config/application.rb +++ b/config/application.rb @@ -33,7 +33,6 @@ class Application < Rails::Application config.x.asembleo.private_instance = ENV['ASEMBLEO_PRIVATE_INSTANCE'].present? config.x.asembleo.open_registration = ENV['ASEMBLEO_OPEN_REGISTRATION'].present? config.x.asembleo.token_alias = ENV['ASEMBLEO_TOKEN_ALIAS'].presence - config.x.asembleo.async_vote = ActiveModel::Type::Boolean.new.cast(ENV['ASEMBLEO_ASYNC_VOTE'].presence) || false config.x.asembleo.hide_receipt = ActiveModel::Type::Boolean.new.cast(ENV['ASEMBLEO_HIDE_RECEIPT'].presence) || false # Branding diff --git a/config/locales/en.yml b/config/locales/en.yml index e111a2a..6503f61 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -81,7 +81,7 @@ en: confirm: Are you sure? create: Create edit: Edit - email: Email + email: &email Email delete: Delete footer: Asembleo is a free software project developed by Coopanio log_in: Log in @@ -105,8 +105,7 @@ en: consultation_created: Consultation created. consultation_draft: The consultation is still in draft mode. consultation_finished: You have answered all the questions. - consultation_open: The consultation is open, but there are no active questions. - Try it again later. + consultation_open: The consultation is open. Please proceed to vote by clicking next. consultation_archived: The consultation has been archived. consultation_closed: The consultation is closed. consultation_deleted: Consultation deleted. @@ -316,6 +315,9 @@ en: session: identifier: Identifier password: Password + user: + email: *email + nid: Identity document events_helper: open: Open close: Close diff --git a/db/migrate/20230421070610_add_voter_to_receipts.rb b/db/migrate/20230421070610_add_voter_to_receipts.rb new file mode 100644 index 0000000..18bcfda --- /dev/null +++ b/db/migrate/20230421070610_add_voter_to_receipts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddVoterToReceipts < ActiveRecord::Migration[7.0] + def change + add_reference :receipts, :voter, polymorphic: true, null: true + end +end diff --git a/db/migrate/20230421070925_backfill_existing_receipts.rb b/db/migrate/20230421070925_backfill_existing_receipts.rb new file mode 100644 index 0000000..73c9a57 --- /dev/null +++ b/db/migrate/20230421070925_backfill_existing_receipts.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class BackfillExistingReceipts < ActiveRecord::Migration[7.0] + disable_ddl_transaction! + + def change + Receipt.unscoped.in_batches do |batch| + batch.update_all(voter_type: 'Token', voter_id: Arel.sql('token_id')) + end + end +end diff --git a/db/migrate/20230421071603_remove_token_from_receipts.rb b/db/migrate/20230421071603_remove_token_from_receipts.rb new file mode 100644 index 0000000..66d10ec --- /dev/null +++ b/db/migrate/20230421071603_remove_token_from_receipts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RemoveTokenFromReceipts < ActiveRecord::Migration[7.0] + def change + remove_reference :receipts, :token, null: false, foreign_key: true + end +end diff --git a/db/migrate/20230421151548_remove_token_and_question_index_from_receipts.rb b/db/migrate/20230421151548_remove_token_and_question_index_from_receipts.rb new file mode 100644 index 0000000..490d79d --- /dev/null +++ b/db/migrate/20230421151548_remove_token_and_question_index_from_receipts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RemoveTokenAndQuestionIndexFromReceipts < ActiveRecord::Migration[7.0] + def change + remove_index :receipts, name: 'index_receipts_on_token_id_and_question_id' + end +end diff --git a/db/migrate/20230421151747_add_voter_and_question_index_to_receipts.rb b/db/migrate/20230421151747_add_voter_and_question_index_to_receipts.rb new file mode 100644 index 0000000..dc97c46 --- /dev/null +++ b/db/migrate/20230421151747_add_voter_and_question_index_to_receipts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddVoterAndQuestionIndexToReceipts < ActiveRecord::Migration[7.0] + def change + add_index :receipts, %i[voter_type voter_id question_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 906780c..ded5a6b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_03_14_232746) do +ActiveRecord::Schema[7.0].define(version: 2023_04_21_151747) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -116,13 +116,14 @@ create_table "receipts", force: :cascade do |t| t.text "fingerprint" - t.integer "token_id", null: false t.integer "question_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "voter_type" + t.integer "voter_id" t.index ["question_id"], name: "index_receipts_on_question_id" - t.index ["token_id", "question_id"], name: "index_receipts_on_token_id_and_question_id", unique: true - t.index ["token_id"], name: "index_receipts_on_token_id" + t.index ["voter_type", "voter_id", "question_id"], name: "index_receipts_on_voter_type_and_voter_id_and_question_id", unique: true + t.index ["voter_type", "voter_id"], name: "index_receipts_on_voter" end create_table "token_receipts", id: false, force: :cascade do |t| @@ -204,7 +205,6 @@ add_foreign_key "question_links", "questions" add_foreign_key "questions", "consultations" add_foreign_key "receipts", "questions" - add_foreign_key "receipts", "tokens" add_foreign_key "token_receipts", "consultations" add_foreign_key "token_tags", "tokens" add_foreign_key "tokens", "consultations" diff --git a/test/controllers/questions_controller_test.rb b/test/controllers/questions_controller_test.rb index b57b6b8..a93cdcf 100644 --- a/test/controllers/questions_controller_test.rb +++ b/test/controllers/questions_controller_test.rb @@ -18,6 +18,7 @@ class QuestionsControllerTest < ActionDispatch::IntegrationTest post sessions_url, params: { session: { identifier: token.to_hash } } post votes_url, params: { vote: { question.id => { value: [value] } }, question_id: question.id }.stringify_keys + perform_enqueued_jobs end end @@ -50,6 +51,7 @@ class QuestionsControllerTest < ActionDispatch::IntegrationTest post sessions_url, params: { session: { identifier: token.to_hash } } post votes_url, params: { vote: { question.id => { value: [value] } }, question_id: question.id }.stringify_keys + perform_enqueued_jobs end results = subject diff --git a/test/controllers/votes_controller_test.rb b/test/controllers/votes_controller_test.rb index 5df711e..2f30b08 100644 --- a/test/controllers/votes_controller_test.rb +++ b/test/controllers/votes_controller_test.rb @@ -20,11 +20,15 @@ class VotesControllerTest < ActionDispatch::IntegrationTest post sessions_url, params: { session: { identifier: token.to_hash } } end - subject { post votes_url, params: @params } + subject do + post votes_url, params: @params + perform_enqueued_jobs + end test 'should create vote' do subject + assert_performed_jobs 1 assert_response :success Vote.all.tap do |votes| @@ -33,7 +37,7 @@ class VotesControllerTest < ActionDispatch::IntegrationTest assert_equal @token.event, votes.first.event end - Receipt.find_by(token:, question:).tap do |receipt| + Receipt.find_by(voter: token, question:).tap do |receipt| assert_predicate receipt, :present? assert receipt.fingerprint.present? && receipt.fingerprint.length == 64 end @@ -51,7 +55,7 @@ class VotesControllerTest < ActionDispatch::IntegrationTest assert(votes.map(&:event).all?(@token.event)) end - Receipt.find_by(token:, question:).tap do |receipt| + Receipt.find_by(voter: token, question:).tap do |receipt| assert_predicate receipt, :present? assert receipt.fingerprint.present? && receipt.fingerprint.length == 64 end @@ -100,23 +104,6 @@ class VotesControllerTest < ActionDispatch::IntegrationTest assert_response :success end - test 'should asynchronously create vote' do - CastVotes.stub_any_instance(:async?, true) do - subject - end - - perform_enqueued_jobs - - assert_performed_jobs 1 - - Vote.all.tap do |votes| - assert_equal 1, votes.length - assert_equal 'yes', votes.first.value - end - - assert_includes response.body, Receipt.first.fingerprint - end - test 'should fail if question_id is unknown' do @params['vote'] = { '999' => { value: [@value] } } @params['question_id'] = 999 @@ -125,7 +112,7 @@ class VotesControllerTest < ActionDispatch::IntegrationTest # Not sure about this, the redirection is caused because not enough questions where found... assert_response :redirect assert_empty Vote.all - assert_not Receipt.exists?(token:, question:) + assert_not Receipt.exists?(voter: token, question:) end test 'should fail if value is not valid' do @@ -134,7 +121,7 @@ class VotesControllerTest < ActionDispatch::IntegrationTest assert_response :redirect assert_empty Vote.all - assert_not Receipt.exists?(token:, question:) + assert_not Receipt.exists?(voter: token, question:) end test 'should fail if a second vote is attempted' do diff --git a/test/factories.rb b/test/factories.rb index ae82deb..c34cf80 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -57,7 +57,7 @@ end factory :receipt do - token + voter { create(:token) } question end end diff --git a/test/interactors/redirect_by_session_test.rb b/test/interactors/redirect_by_session_test.rb index 3a7b8d8..4cf092b 100644 --- a/test/interactors/redirect_by_session_test.rb +++ b/test/interactors/redirect_by_session_test.rb @@ -51,8 +51,8 @@ class RedirectBySessionTest < ActiveSupport::TestCase init_questions(status, event) end - create(:receipt, token: identity, question: questions.first) - create(:receipt, token: identity, question: questions.second) + create(:receipt, voter: identity, question: questions.first) + create(:receipt, voter: identity, question: questions.second) assert_predicate subject, :present? assert_equal subject, "/consultations/#{consultation.id}/questions/#{questions.third.id}" diff --git a/test/mailers/sessions_mailer_test.rb b/test/mailers/sessions_mailer_test.rb index d4c3e60..7bfda13 100644 --- a/test/mailers/sessions_mailer_test.rb +++ b/test/mailers/sessions_mailer_test.rb @@ -6,7 +6,7 @@ class SessionsMailerTest < ActionMailer::TestCase test 'generate magic link email' do to = 'user@coopanio.com' token = create(:token) - email = SessionsMailer.magic_link_email(to, token) + email = SessionsMailer.magic_link_email(to, token.class.name, token.id, token.to_hash) assert_emails 1 do email.deliver_now diff --git a/test/services/fingerprint_service_test.rb b/test/services/fingerprint_service_test.rb index 0e1a991..3c3b490 100644 --- a/test/services/fingerprint_service_test.rb +++ b/test/services/fingerprint_service_test.rb @@ -9,7 +9,7 @@ class FingerprintServiceTest < ActiveSupport::TestCase Timecop.freeze(Time.utc(2020, 9, 11, 0, 0)) do @consultation = create(:consultation, id: 1) @token = create(:token, id: 1, consultation: @consultation, salt: 9999) - @receipt = create(:receipt, id: nil, token:) + @receipt = create(:receipt, id: nil, voter: @token) end end @@ -18,12 +18,12 @@ class FingerprintServiceTest < ActiveSupport::TestCase test 'generate' do @short = false - assert_equal '050893424e6e39c6cecf0ae010185be7af5e971b5ccc2235c68d96218a5a38cd', subject + assert_equal 'f513eba3cdcb251c01f5cfd8ccea0e5f4c388dea74f37645cc3e77491c21c2d5', subject end test 'generate short' do @short = true - assert_equal '050893424e6e39c6cecf', subject + assert_equal 'f513eba3cdcb251c01f5', subject end end diff --git a/test/services/markdown_service_test.rb b/test/services/markdown_service_test.rb index 04589b9..6116c6b 100644 --- a/test/services/markdown_service_test.rb +++ b/test/services/markdown_service_test.rb @@ -12,7 +12,7 @@ class MarkdownServiceTest < ActiveSupport::TestCase test 'render' do rendered = MarkdownService.render(text) - assert_equal(%(This is a test), rendered) + assert_equal(%(

This is a test

), rendered) end test 'render plain text' do