Skip to content

Commit

Permalink
feat: Unconfirmed votes cleaner (#86)
Browse files Browse the repository at this point in the history
* feat: Create Confirmation Reminder Event

* feat: Create confirmation reminder job

* fix: Docker-compose local change database_name

* lint: Fix rubocop offenses

* feat: Create confirmation email

* feat: Clear unconfirmed votes

* fix: Locales

* fix: Allow to configure delays

* fix: ConfirmationReminderJob

* feat: Define Sidekiq crons

* fix: Remove unused events

* revert: Remove AnswerInitiativeEvent

* lint: Fix rubocop offense

* fix: Remove unused locales

---------

Co-authored-by: AyakorK <[email protected]>
  • Loading branch information
Quentinchampenois and AyakorK authored Dec 6, 2023
1 parent 1ecb486 commit 8c15073
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 0 deletions.
17 changes: 17 additions & 0 deletions app/jobs/decidim/confirmation_reminder_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Decidim
class ConfirmationReminderJob < ApplicationJob
def perform
unconfirmed_users.each do |user|
Decidim::ConfirmationReminderMailer.send_reminder(user).deliver_now
end
end

private

def unconfirmed_users
@unconfirmed_users ||= Decidim::User.not_confirmed.where("DATE(created_at) = ?", Rails.application.secrets.dig(:decidim, :reminder, :unconfirmed_email, :days).days.ago)
end
end
end
26 changes: 26 additions & 0 deletions app/jobs/decidim/unconfirmed_votes_cleaner_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Decidim
class UnconfirmedVotesCleanerJob < ApplicationJob
def perform
unconfirmed_users.each do |user|
votes = Decidim::InitiativesVote.includes(:initiative)
.where(author: user)
next if votes.blank?

initiatives = votes.map(&:initiative)
votes.destroy_all
Decidim::UnconfirmedVotesClearMailer.send_resume(user, initiatives).deliver_now
end
end

private

def unconfirmed_users
@unconfirmed_users ||= Decidim::User.not_confirmed
.where("DATE(decidim_users.created_at) = ?", Decidim.unconfirmed_access_for.ago.to_date)
.joins("JOIN decidim_initiatives_votes ON decidim_users.id = decidim_initiatives_votes.decidim_author_id")
.distinct
end
end
end
22 changes: 22 additions & 0 deletions app/mailers/decidim/confirmation_reminder_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Decidim
class ConfirmationReminderMailer < ApplicationMailer
helper Decidim::SanitizeHelper
helper Decidim::TranslationsHelper

def send_reminder(user)
return if user&.email.blank?

@organization = user.organization
@user = user
root_url = decidim.root_url(host: @organization.host)[0..-2]
@confirmation_link = "#{root_url}#{decidim.user_confirmation_path(confirmation_token: user.confirmation_token)}"
with_user(user) do
@subject = I18n.t("subject", scope: "decidim.confirmation_reminder_mailer.send_reminder")

mail(to: "#{user.name} <#{user.email}>", subject: @subject)
end
end
end
end
22 changes: 22 additions & 0 deletions app/mailers/decidim/unconfirmed_votes_clear_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Decidim
class UnconfirmedVotesClearMailer < ApplicationMailer
helper Decidim::SanitizeHelper
helper Decidim::TranslationsHelper

def send_resume(user, initiatives)
return if user&.email.blank?

@organization = user.organization
@user = user
@initiatives = initiatives

with_user(user) do
@subject = I18n.t("subject", scope: "decidim.unconfirmed_votes_clear_mailer.send_resume")

mail(to: "#{user.name} <#{user.email}>", subject: @subject)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

<p>
<%== t "title", scope: "decidim.confirmation_reminder_mailer.send_reminder.body" %>
</p>

<p>
<%== t "body", scope: "decidim.confirmation_reminder_mailer.send_reminder.body" %>
</p>

<div>
<%= link_to t("confirmation_link", scope: "decidim.confirmation_reminder_mailer.send_reminder.body"), @confirmation_link %>
</div>

<p>
<%== t "warning", scope: "decidim.confirmation_reminder_mailer.send_reminder.body" %>
</p>

<% content_for :note do %>
<%== t "subject", scope: "decidim.confirmation_reminder_mailer.send_reminder", organization_name: h(@organization.name), link: decidim.notifications_settings_url(host: @organization.host) %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

<p>
<%== t "title", scope: "decidim.unconfirmed_votes_clear_mailer.send_resume.body" %>
</p>

<ul>
<% @initiatives.each do |initiative| %>
<li>
<%== t("vote", scope: "decidim.unconfirmed_votes_clear_mailer.send_resume.body", initiative_title: translated_attribute(initiative.title)) %>
</li>
<% end %>
</ul>


<p>
<%== t "confirm", scope: "decidim.unconfirmed_votes_clear_mailer.send_resume.body" %>
</p>


<% content_for :note do %>
<%== t "subject", scope: "decidim.confirmation_reminder_mailer.send_reminder", organization_name: h(@organization.name), link: decidim.notifications_settings_url(host: @organization.host) %>
<% end %>
15 changes: 15 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ en:
name: Identity Verification Form
osp_authorization_workflow:
name: Authorization procedure
confirmation_reminder_mailer:
send_reminder:
body:
body: Please confirm your email address to persist your votes.
confirmation_link: Link to confirmation
title: You have created an account there is 2 days ago and you haven't confirmed your email yet.
warning: If you don't confirm your email address, your votes will be deleted.
subject: Please confirm your email address
devise:
registrations:
form:
Expand Down Expand Up @@ -204,6 +212,13 @@ en:
client_id: Client ID
client_secret: Client secret
site_url: Site URL
unconfirmed_votes_clear_mailer:
send_resume:
body:
confirm: You can still confirm your account and vote again on initiatives.
title: You did not confirm your account, existing votes have been deleted
vote: Your vote on %{initiative_title} has been deleted.
subject: You did not confirm your account, existing votes have been deleted
verifications:
authorizations:
create:
Expand Down
15 changes: 15 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ fr:
name: Formulaire de vérification d'identité
osp_authorization_workflow:
name: Procédure d'autorisation
confirmation_reminder_mailer:
send_reminder:
body:
body: Veuillez confirmer votre compte en cliquant sur le lien ci-dessous.
confirmation_link: Lien de confirmation
title: Vous avez créé un compte il y a 2 jours mais vous ne l'avez pas encore confirmé.
warning: L'ensemble de vos votes seront supprimés dans 6 jours si votre compte n'est pas confirmé dans ce délai.
subject: Veuillez confirmer votre compte
devise:
registrations:
form:
Expand Down Expand Up @@ -200,6 +208,13 @@ fr:
client_id: Client ID
client_secret: Client secret
site_url: Site URL
unconfirmed_votes_clear_mailer:
send_resume:
body:
confirm: Vous pouvez toujours confirmer votre compte et voter à nouveau sur les pétitions.
title: Vous n'avez pas confirmé votre compte, les votes que vous avez fait ont été supprimés
vote: Votre vote sur la pétition '%{initiative_title}' a été supprimé.
subject: Vous n'avez pas confirmé votre compte, vos votes ont été supprimés
verifications:
authorizations:
create:
Expand Down
3 changes: 3 additions & 0 deletions config/secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
default: &default
asset_host: <%= ENV["ASSET_HOST"] %>
decidim:
reminder:
unconfirmed_email:
days: <%= ENV["DECIDIM_REMINDER_UNCONFIRMED_EMAIL_DAYS"]&.to_i || 2 %>
verifications:
sms_gateway_service:
username: <%= ENV["SMS_GATEWAY_USERNAME"].presence %>
Expand Down
8 changes: 8 additions & 0 deletions config/sidekiq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
cron: '0 0 1 * * *' # Run at 01:00
class: PreloadOpenDataJob
queue: scheduled
ConfirmationReminderJob:
cron: '0 0 9 * * *' # Run at 09:00
class: Decidim::ConfirmationReminderJob
queue: scheduled
UnconfirmedVotesCleanerJob:
cron: '0 0 9 * * *' # Run at 09:00
class: Decidim::UnconfirmedVotesCleanerJob
queue: scheduled
DetectSpamUsers:
cron: '0 <%= Random.rand(0..59) %> <%= Random.rand(6..8) %> * * *' # Run randomly between 06:00 and 08:59
class: Decidim::SpamDetection::MarkUsersJob
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ services:
environment:
- DATABASE_HOST=database
- DATABASE_USERNAME=postgres
- DATABASE_NAME=decidim_cese
- DECIDIM_HOST=localhost
- REDIS_URL=redis://redis:6379
- MEMCACHE_SERVERS=memcached:11211
Expand Down Expand Up @@ -53,6 +54,7 @@ services:
dockerfile: Dockerfile.local
environment:
- DATABASE_HOST=database
- DATABASE_NAME=decidim_cese
- DATABASE_USERNAME=postgres
- DECIDIM_HOST=localhost
- REDIS_URL=redis://redis:6379
Expand Down
36 changes: 36 additions & 0 deletions spec/jobs/decidim/confirmation_reminder_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require "spec_helper"

describe Decidim::ConfirmationReminderJob do
subject { described_class }

let!(:unconfirmed_users) { create_list(:user, 2, created_at: 2.days.ago) }

before do
create(:user, created_at: 3.days.ago)
create(:user, :confirmed, created_at: 2.days.ago)
create(:user, created_at: 1.day.ago)
end

it "sends a reminder to unconfirmed users" do
expect { subject.new.perform }.to change { ActionMailer::Base.deliveries.count }.by(2)
end

context "when confirmation reminder is set to 3 days" do
before do
allow(Rails.application.secrets).to receive(:dig).and_call_original
allow(Rails.application.secrets).to receive(:dig).with(:decidim, :reminder, :unconfirmed_email, :days).and_return(3)
end

it "send a unique email to user created there is 3 days ago" do
expect { subject.new.perform }.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end

describe "#unconfirmed_users" do
it "returns the unconfirmed users" do
expect(subject.new.send(:unconfirmed_users)).to match_array(unconfirmed_users)
end
end
end
31 changes: 31 additions & 0 deletions spec/jobs/decidim/unconfirmed_votes_cleaner_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require "spec_helper"

describe Decidim::UnconfirmedVotesCleanerJob do
subject { described_class }

let!(:unconfirmed_users) { create_list(:user, 2, created_at: 7.days.ago) }
let!(:confirmed_user) { create(:user, :confirmed, created_at: 7.days.ago) }
let!(:unconfirmed_votes) { create_list(:initiative_user_vote, 3, author: unconfirmed_users.first) }
let!(:confirmed_votes) { create_list(:initiative_user_vote, 3, author: confirmed_user) }

before do
allow(Decidim.unconfirmed_access_for).to receive(:ago).and_return(7.days.ago)
create(:user, created_at: 30.days.ago)
create(:user, :confirmed, created_at: 2.days.ago)
create(:user, created_at: 1.day.ago)
end

it "sends a reminder to unconfirmed users" do
expect { subject.new.perform }.to change { ActionMailer::Base.deliveries.count }.by(1)
end

describe "#unconfirmed_users" do
it "returns the unconfirmed user with initiatives votes" do
expect(subject.new.send(:unconfirmed_users).count).to eq(1)
expect(subject.new.send(:unconfirmed_users)).to include(unconfirmed_users.first)
expect(subject.new.send(:unconfirmed_users)).not_to include(unconfirmed_users.last)
end
end
end
41 changes: 41 additions & 0 deletions spec/mailers/confirmation_reminder_mailer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require "spec_helper"

module Decidim
describe ConfirmationReminderMailer, type: :mailer do
let(:user) { create(:user, name: "Sarah Connor", organization: organization) }
let(:organization) { create(:organization) }

describe "#send_reminder" do
let(:mail) { described_class.send_reminder(user) }

it "parses the subject" do
expect(mail.subject).to eq("Please confirm your email address")
end

it "parses the body" do
expect(email_body(mail)).to include("You have created an account there is 2 days ago and you haven't confirmed your email yet.")
expect(email_body(mail)).to include("Please confirm your email address to persist your votes.")
expect(email_body(mail)).to include("If you don't confirm your email address, your votes will be deleted.")
end

context "when the user has a different locale" do
before do
user.locale = "fr"
user.save!
end

it "parses the subject in the user's locale" do
expect(mail.subject).to eq("Veuillez confirmer votre compte")
end

it "parses the body in the user's locale" do
expect(email_body(mail)).to include("Vous avez créé un compte il y a 2 jours mais vous ne l'avez pas encore confirmé.")
expect(email_body(mail)).to include("Veuillez confirmer votre compte en cliquant sur le lien ci-dessous.")
expect(email_body(mail)).to include("L'ensemble de vos votes seront supprimés dans 6 jours si votre compte n'est pas confirmé dans ce délai.")
end
end
end
end
end
50 changes: 50 additions & 0 deletions spec/mailers/unconfirmed_votes_clear_mailer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require "spec_helper"

module Decidim
describe UnconfirmedVotesClearMailer, type: :mailer do
let(:organization) { create(:organization) }
let(:user) { create(:user, name: "Sarah Connor", organization: organization) }
let(:unconfirmed_votes) { create_list(:initiative_user_vote, 3, author: user) }
let(:initiatives) { unconfirmed_votes.map(&:initiative) }

let(:confirmed_user) { create(:user, :confirmed, organization: organization) }
let(:confirmed_votes) { create_list(:initiative_user_vote, 3, author: confirmed_user) }

describe "#send_resume" do
let(:mail) { described_class.send_resume(user, initiatives) }

it "parses the subject" do
expect(mail.subject).to eq("You did not confirm your account, existing votes have been deleted")
end

it "parses the body" do
expect(email_body(mail)).to include("You did not confirm your account, existing votes have been deleted")
expect(email_body(mail)).to include("You can still confirm your account and vote again on initiatives.")
initiatives.each do |initiative|
expect(email_body(mail)).to include("Your vote on #{translated(initiative.title)} has been deleted.")
end
end

context "when the user has a different locale" do
before do
user.locale = "fr"
user.save!
end

it "parses the subject in the user's locale" do
expect(mail.subject).to eq("Vous n'avez pas confirmé votre compte, vos votes ont été supprimés")
end

it "parses the body in the user's locale" do
expect(email_body(mail)).to include("Vous n'avez pas confirmé votre compte, les votes que vous avez fait ont été supprimés")
expect(email_body(mail)).to include("Vous pouvez toujours confirmer votre compte et voter à nouveau sur les pétitions.")
initiatives.each do |initiative|
expect(email_body(mail)).to include("Votre vote sur la pétition '#{translated(initiative.title)}' a été supprimé.")
end
end
end
end
end
end

0 comments on commit 8c15073

Please sign in to comment.