diff --git a/app/controllers/admin/mass_actions_controller.rb b/app/controllers/admin/mass_actions_controller.rb new file mode 100644 index 000000000..bc3491faf --- /dev/null +++ b/app/controllers/admin/mass_actions_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Admin + class MassActionsController < BaseController + before_action :authorize_admin + + # GET /admin/mass_actions + def index; end + + # POST /admin/mass_actions + def create + res = MassAction.process(params[:mass_action], params[:entry_list].path) + notice = if res + "#{params[:mass_action]} completed for #{res[:ok]}.\n" \ + "Failed: #{res[:fail]}" + else + "Dataset integrity validation failed for #{params[:mass_action]}" + end + + redirect_to(admin_mass_actions_path, notice: notice) + end + + def authorize_admin + authorize! :manage, :mass_actions + end + end +end diff --git a/app/interactions/domains/force_delete/base.rb b/app/interactions/domains/force_delete/base.rb index 27601c1d2..d4ad2b820 100644 --- a/app/interactions/domains/force_delete/base.rb +++ b/app/interactions/domains/force_delete/base.rb @@ -10,6 +10,9 @@ module Domains boolean :notify_by_email, default: false, description: 'Do we need to send email notification' + string :reason, + default: nil, + description: 'Which mail template to use explicitly' validates :type, inclusion: { in: %i[fast_track soft] } end diff --git a/app/interactions/domains/force_delete/notify_by_email.rb b/app/interactions/domains/force_delete/notify_by_email.rb index b60f54a5e..e512657b0 100644 --- a/app/interactions/domains/force_delete/notify_by_email.rb +++ b/app/interactions/domains/force_delete/notify_by_email.rb @@ -8,7 +8,7 @@ module Domains send_email domain.update(contact_notification_sent_date: Time.zone.today) else - domain.update(template_name: domain.notification_template) + domain.update(template_name: domain.notification_template(explicit: reason)) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 31637b8ea..0bee01f9c 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -109,6 +109,7 @@ class Ability can :destroy, :pending can :create, :zonefile can :access, :settings_menu + can :manage, :mass_actions can :manage, BouncedMailAddress end diff --git a/app/models/concerns/domain/force_delete.rb b/app/models/concerns/domain/force_delete.rb index e06da25cc..87e9a957b 100644 --- a/app/models/concerns/domain/force_delete.rb +++ b/app/models/concerns/domain/force_delete.rb @@ -19,7 +19,10 @@ module Concerns::Domain::ForceDelete # rubocop:disable Metrics/ModuleLength end end - def notification_template + def notification_template(explicit: nil) + reason = explicit&.downcase + return reason if %w[invalid_email invalid_phone].include?(reason) + if contact_emails_verification_failed.present? 'invalid_email' elsif registrant.org? @@ -33,9 +36,8 @@ module Concerns::Domain::ForceDelete # rubocop:disable Metrics/ModuleLength statuses.include?(DomainStatus::FORCE_DELETE) end - def schedule_force_delete(type: :fast_track, notify_by_email: false) - Domains::ForceDelete::SetForceDelete.run(domain: self, - type: type, + def schedule_force_delete(type: :fast_track, notify_by_email: false, reason: nil) + Domains::ForceDelete::SetForceDelete.run(domain: self, type: type, reason: reason, notify_by_email: notify_by_email) end diff --git a/app/models/mass_action.rb b/app/models/mass_action.rb new file mode 100644 index 000000000..c9cc1fe14 --- /dev/null +++ b/app/models/mass_action.rb @@ -0,0 +1,42 @@ +class MassAction + def self.process(action_type, entries) + entries = CSV.read(entries, headers: true) + case action_type + when 'force_delete' + process_force_delete(entries) + else + false + end + rescue StandardError + false + end + + def self.process_force_delete(entries) + return false unless force_delete_entries_valid?(entries) + + apply_force_deletes(entries) + end + + def self.apply_force_deletes(entries) + log = { ok: [], fail: [] } + entries.each do |e| + dn = Domain.find_by(name_puny: e['domain_name']) + log[:fail] << e['domain_name'] and next unless dn + + dn.schedule_force_delete(type: :soft, notify_by_email: true, reason: e['delete_reason']) + + log[:ok] << dn.name + end + + log + end + + def self.force_delete_entries_valid?(entries) + entries.each do |e| + reasons = %w[ENTITY_BURIED INVALID_EMAIL INVALID_PHONE] + return false unless e['domain_name'].present? && reasons.include?(e['delete_reason']) + end + + true + end +end diff --git a/app/views/admin/base/_menu.haml b/app/views/admin/base/_menu.haml index 5853bd3e6..46910afa7 100644 --- a/app/views/admin/base/_menu.haml +++ b/app/views/admin/base/_menu.haml @@ -33,6 +33,7 @@ %li= link_to t('.blocked_domains'), admin_blocked_domains_path %li= link_to t('.reserved_domains'), admin_reserved_domains_path %li= link_to t('.disputed_domains'), admin_disputes_path + %li= link_to t('.bulk_actions'), admin_mass_actions_path %li= link_to t('.bounced_email_addresses'), admin_bounced_mail_addresses_path %li= link_to t('.epp_log'), admin_epp_logs_path(created_after: 'today') %li= link_to t('.repp_log'), admin_repp_logs_path(created_after: 'today') diff --git a/app/views/admin/mass_actions/index.html.erb b/app/views/admin/mass_actions/index.html.erb new file mode 100644 index 000000000..22ddf6bbb --- /dev/null +++ b/app/views/admin/mass_actions/index.html.erb @@ -0,0 +1,19 @@ + + +
+
Bulk Domain Force Delete
+
+

Triggers soft force delete procedure for uploaded domain list. List must be in CSV format. First row of the CSV file must contain column headings with domain_name for the first and delete_reason for the second column. Each domain entry must be on separate line. Domain names are expected to be in punycode format, valid reasons are listed below.

+

Allowed delete reasons: ENTITY_BURIED | INVALID_PHONE | INVALID_EMAIL

+ <%= form_tag admin_mass_actions_path, multipart: true, method: :post do %> + <%= label_tag :entry_list %> + <%= file_field_tag :entry_list, required: true, accept: 'text/csv' %> + <%= hidden_field_tag :mass_action, 'force_delete' %> + <%= hidden_field_tag :authenticity_token, form_authenticity_token %> +
+ <%= submit_tag "Start force delete process", class: 'btn btn-danger', id: 'fd_submit' %> + <% end %> +
+
diff --git a/app/views/mailers/domain_delete_mailer/forced/invalid_phone.html.erb b/app/views/mailers/domain_delete_mailer/forced/invalid_phone.html.erb new file mode 100644 index 000000000..746a0e256 --- /dev/null +++ b/app/views/mailers/domain_delete_mailer/forced/invalid_phone.html.erb @@ -0,0 +1,47 @@ +

Lugupeetud domeeni <%= @domain.name %> registreerija/halduskontakt

+ +

Eesti Interneti Sihtasutusele (EIS) on saanud teatavaks, et domeeni <%= @domain.name %> kontakti(de) telefoni number või numbrid on puudulikud.

+ +

Et see olukord on vastuolus .ee domeenireeglitega algatas EIS <%= @delete_period_length %> päeva pikkuse kustutusmenetluse. Menetluse käigus on domeen <%= @expire_warning_period %> esimest päeva internetis kättesaadav.

+ +

Andmete parandamiseks pöörduge palun oma registripidaja <%= @registrar.name %> poole või isiklike ja oma ettevõtte andmete puhul registreerija portaali.

+ +

Kui kontaktandmed ei ole <%= @delete_period_length %> päeva jooksul parandatud, läheb domeen <%= @domain.name %> <%= @domain.force_delete_date %> domeenioksjonile .ee oksjonikeskkonda. Juhul kui domeenile <%= @domain.name %> ei tehta oksjonil 24h möödudes pakkumist, domeen vabaneb ja on registreerimiseks vabalt kättesaadav kõigile huvilistele. Muude võimalike oksjoni tulemuste kohta loe siit.

+ +

Lisaküsimuste korral võtke palun ühendust oma registripidajaga:

+<%= render 'mailers/shared/registrar/registrar.et.html', registrar: @registrar %> + +<%= render 'mailers/shared/signatures/signature.et.html' %> + +
+ +

Dear registrant/administrative contact of .ee domain,

+ +

Estonian Internet Foundation has learned that contact(s) phone number data of the domain <%= @domain.name %> are invalid.

+ +

Since this is a violation of Estonian domain regulations, <%= @delete_period_length %>-day deletion process has started for the <%= @domain.name %> domain. For the first <%= @expire_warning_period %> days the domain will remain available on the Internet during the deletion process.

+ +

Please, contact your registrar <%= @registrar.name %> with updated contact data, or in case of your personal or business data use .ee portal for registrants

+ +

If the data is not fixed within <%= @delete_period_length %> days, the domain <%= @domain.name %> will go to domain auction on <%= @domain.force_delete_date %> in the .ee auction environment. If no offer is made for the domain <%= @domain.name %> at auction within 24 hours, the domain will be released and made freely available for registration to anyone interested on a first-come, first-served basis. Read more about other potential auction results here.

+ +

Should you have additional questions, please contact your registrar:

+<%= render 'mailers/shared/registrar/registrar.en.html', registrar: @registrar %> + +<%= render 'mailers/shared/signatures/signature.en.html' %> +
+ +

Уважаемый регистрант/административный контакт домена .ee

+ +

Целевому учреждению Eesti Internet (EIS) стало известно, что контактные данные домена <%= @domain.name %> неверны - телефонные номера.

+ +

Так как это является нарушением Правил домена .ee, <%= @delete_period_length %>-дневный процесс удаления начат для доменного имени <%= @domain.name %>. В течение первых <%= @expire_warning_period %> дней домен будет доступен в интернете.

+ +

Для уточнения контактных данных, пожалуйста, свяжитесь с регистратором <%= @registrar.name %>, либо воспользуйтесь порталом для регистрантов

+ +

Если контактные данные не будут исправлены в течение <%= @delete_period_length %> дней, домен <%= @domain.name %> отправится <%= @domain.force_delete_date %> на доменный аукцион в аукционной среде.ee. Если в течение 24 часов в отношении домена <%= @domain.name %> е поступит предложений, домен освободится и станет доступным для всех желающих по принципу «кто раньше». О других возможных результатах аукциона читайте здесь.

+ +

В случае возникновения дополнительных вопросов свяжитесь, пожалуйста, со своим регистратором: +<%= render 'mailers/shared/registrar/registrar.ru.html', registrar: @registrar %>

+ +<%= render 'mailers/shared/signatures/signature.ru.html' %> diff --git a/app/views/mailers/domain_delete_mailer/forced/invalid_phone.text.erb b/app/views/mailers/domain_delete_mailer/forced/invalid_phone.text.erb new file mode 100644 index 000000000..fe61b44d1 --- /dev/null +++ b/app/views/mailers/domain_delete_mailer/forced/invalid_phone.text.erb @@ -0,0 +1,47 @@ +

Lugupeetud domeeni <%= @domain.name %> registreerija/halduskontakt

+ +

Eesti Interneti Sihtasutusele (EIS) on saanud teatavaks, et domeeni <%= @domain.name %> kontakti(de) telefoni number või numbrid on puudulikud.

+ +

Et see olukord on vastuolus .ee domeenireeglitega algatas EIS <%= @delete_period_length %> päeva pikkuse kustutusmenetluse. Menetluse käigus on domeen <%= @expire_warning_period %> esimest päeva internetis kättesaadav.

+ +

Andmete parandamiseks pöörduge palun oma registripidaja <%= @registrar.name %> poole või isiklike ja oma ettevõtte andmete puhul registreerija portaali.

+ +

Kui kontaktandmed ei ole <%= @delete_period_length %> päeva jooksul parandatud, läheb domeen <%= @domain.name %> <%= @domain.force_delete_date %> domeenioksjonile .ee oksjonikeskkonda. Juhul kui domeenile <%= @domain.name %> ei tehta oksjonil 24h möödudes pakkumist, domeen vabaneb ja on registreerimiseks vabalt kättesaadav kõigile huvilistele. Muude võimalike oksjoni tulemuste kohta loe siit.

+ +

Lisaküsimuste korral võtke palun ühendust oma registripidajaga:

+<%= render 'mailers/shared/registrar/registrar.et.html', registrar: @registrar %> + +<%= render 'mailers/shared/signatures/signature.et.html' %> + +
+ +

Dear registrant/administrative contact of .ee domain,

+ +

Estonian Internet Foundation has learned that contact(s) phone number data of the domain <%= @domain.name %> are invalid.

+ +

Since this is a violation of Estonian domain regulations, <%= @delete_period_length %>-day deletion process has started for the <%= @domain.name %> domain. For the first <%= @expire_warning_period %> days the domain will remain available on the Internet during the deletion process.

+ +

Please, contact your registrar <%= @registrar.name %> with updated contact data, or in case of your personal or business data use .ee portal for registrants

+ +

If the data is not fixed within <%= @delete_period_length %> days, the domain <%= @domain.name %> will go to domain auction on <%= @domain.force_delete_date %> in the .ee auction environment. If no offer is made for the domain <%= @domain.name %> at auction within 24 hours, the domain will be released and made freely available for registration to anyone interested on a first-come, first-served basis. Read more about other potential auction results here.

+ +

Should you have additional questions, please contact your registrar:

+<%= render 'mailers/shared/registrar/registrar.en.html', registrar: @registrar %> + +<%= render 'mailers/shared/signatures/signature.en.html' %> +
+ +

Уважаемый регистрант/административный контакт домена .ee

+ +

Целевому учреждению Eesti Internet (EIS) стало известно, что контактные данные домена <%= @domain.name %> неверны - телефонные номера.

+ +

Так как это является нарушением Правил домена .ee, <%= @delete_period_length %>-дневный процесс удаления начат для доменного имени <%= @domain.name %>. В течение первых <%= @expire_warning_period %> дней домен будет доступен в интернете.

+ +

Для уточнения контактных данных, пожалуйста, свяжитесь с регистратором <%= @registrar.name %>, либо воспользуйтесь порталом для регистрантов

+ +

Если контактные данные не будут исправлены в течение <%= @delete_period_length %> дней, домен <%= @domain.name %> отправится <%= @domain.force_delete_date %> на доменный аукцион в аукционной среде.ee. Если в течение 24 часов в отношении домена <%= @domain.name %> е поступит предложений, домен освободится и станет доступным для всех желающих по принципу «кто раньше». О других возможных результатах аукциона читайте здесь.

+ +

В случае возникновения дополнительных вопросов свяжитесь, пожалуйста, со своим регистратором: + <%= render 'mailers/shared/registrar/registrar.ru.html', registrar: @registrar %>

+ +<%= render 'mailers/shared/signatures/signature.ru.html' %> diff --git a/config/locales/admin/menu.en.yml b/config/locales/admin/menu.en.yml index cb1060e6f..52f0a210b 100644 --- a/config/locales/admin/menu.en.yml +++ b/config/locales/admin/menu.en.yml @@ -14,6 +14,7 @@ en: blocked_domains: Blocked domains reserved_domains: Reserved domains disputed_domains: Disputed domains + bulk_actions: Bulk actions bounced_email_addresses: Bounced emails epp_log: EPP log repp_log: REPP log diff --git a/config/routes.rb b/config/routes.rb index c31682a13..3042eced4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -322,6 +322,7 @@ Rails.application.routes.draw do resources :delayed_jobs resources :epp_logs resources :repp_logs + resources :mass_actions, only: %i[index create] resources :bounced_mail_addresses, only: %i[index show destroy] authenticate :admin_user do diff --git a/test/fixtures/files/mass_actions/invalid_mass_force_delete_list.csv b/test/fixtures/files/mass_actions/invalid_mass_force_delete_list.csv new file mode 100644 index 000000000..ef5e4c0e3 --- /dev/null +++ b/test/fixtures/files/mass_actions/invalid_mass_force_delete_list.csv @@ -0,0 +1,2 @@ +domain_name,delete_reason +sh\á;[]c' diff --git a/test/fixtures/files/mass_actions/valid_mass_force_delete_list.csv b/test/fixtures/files/mass_actions/valid_mass_force_delete_list.csv new file mode 100644 index 000000000..ba88abeb4 --- /dev/null +++ b/test/fixtures/files/mass_actions/valid_mass_force_delete_list.csv @@ -0,0 +1,5 @@ +domain_name,delete_reason +shop.test,ENTITY_BURIED +airport.test,INVALID_PHONE +library.test,INVALID_EMAIL +nonexistant.test,ENTITY_BURIED diff --git a/test/system/admin_area/mass_actions/mass_force_delete_test.rb b/test/system/admin_area/mass_actions/mass_force_delete_test.rb new file mode 100644 index 000000000..d188b35fd --- /dev/null +++ b/test/system/admin_area/mass_actions/mass_force_delete_test.rb @@ -0,0 +1,24 @@ +require 'application_system_test_case' +require 'test_helper' + +class AdminAreaMassActionsForceDeleteTest < ApplicationSystemTestCase + def setup + sign_in users(:admin) + end + + def test_processes_uploaded_valid_csv + visit admin_mass_actions_path + + attach_file('entry_list', Rails.root.join('test', 'fixtures', 'files', 'mass_actions', 'valid_mass_force_delete_list.csv').to_s) + click_link_or_button 'Start force delete process' + assert_text 'force_delete completed for ["shop.test", "airport.test", "library.test"]. Failed: ["nonexistant.test"]' + end + + def test_processes_uploaded_invalid_csv + visit admin_mass_actions_path + + attach_file(:entry_list, Rails.root.join('test', 'fixtures', 'files', 'mass_actions', 'invalid_mass_force_delete_list.csv').to_s) + click_link_or_button 'Start force delete process' + assert_text 'Dataset integrity validation failed for force_delete' + end +end