From 54be21729b1cf629f0cdba47d24bd7ba49177176 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 7 Jan 2020 17:18:49 +0100 Subject: [PATCH] batch delete periodic events --- CHANGELOG.md | 1 + .../javascripts/controllers/events.js.erb | 98 +++++++++++++++---- .../templates/events/deleteRecurrent.html | 26 +++++ app/controllers/api/events_controller.rb | 7 +- app/services/event_service.rb | 31 ++++++ config/locales/app.public.en.yml | 9 +- config/locales/app.public.es.yml | 8 +- config/locales/app.public.fr.yml | 8 +- config/locales/app.public.pt.yml | 8 +- 9 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 app/assets/templates/events/deleteRecurrent.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 639eb10c0..2784e45b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Automated setup assistant - An administrator can delete a member - An event reservation can be cancelled, if reservation cancellation is enabled +- Batch delete recurring events - Ability to import iCalendar agendas in the public calendar, through URLs to ICS files (RFC 5545) - Ability to configure the duration of a reservation slot, using `SLOT_DURATION`. Previously, only 60 minutes slots were allowed - Ability to force the email validation when a new user registers. This is optionally configured with `USER_CONFIRMATION_NEEDED_TO_SIGN_IN` diff --git a/app/assets/javascripts/controllers/events.js.erb b/app/assets/javascripts/controllers/events.js.erb index 4844499d1..1de803312 100644 --- a/app/assets/javascripts/controllers/events.js.erb +++ b/app/assets/javascripts/controllers/events.js.erb @@ -183,26 +183,22 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' * @param event {$resource} angular's Event $resource */ $scope.deleteEvent = function (event) { - dialogs.confirm({ + // open a confirmation dialog + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '<%= asset_path "events/deleteRecurrent.html" %>', + size: 'md', + controller: 'DeleteRecurrentEventController', resolve: { - object () { - return { - title: _t('app.public.events_show.confirmation_required'), - msg: _t('app.public.events_show.do_you_really_want_to_delete_this_event') - }; - } + eventPromise: ['Event', function (Event) { return Event.get({ id: $scope.event.id }).$promise; }] } - }, function () { - // the admin has confirmed, delete - event.$delete(function () { + }); + // once the dialog was closed, do things depending on the result + modalInstance.result.then(function (res) { + if (res.status == 'success') { $state.go('app.public.events_list'); - return growl.info(_t('app.public.events_show.event_successfully_deleted')); - }, function (error) { - console.error(error); - growl.error(_t('app.public.events_show.unable_to_delete_the_event_because_some_users_alredy_booked_it')); - }); - } - ); + } + }); }; /** @@ -816,3 +812,71 @@ function __range__ (left, right, inclusive) { } return range; } + + +/** + * Controller used in the event deletion modal window + */ +Application.Controllers.controller('DeleteRecurrentEventController', ['$scope', '$uibModalInstance', 'Event', 'eventPromise', 'growl', '_t', + function ($scope, $uibModalInstance, Event, eventPromise, growl, _t) { + + // is the current event (to be deleted) recurrent? + $scope.isRecurrent = eventPromise.recurrence_events.length > 0; + + // with recurrent slots: how many slots should we delete? + $scope.deleteMode = 'single'; + + /** + * Confirmation callback + */ + $scope.ok = function () { + const { id, start_at, end_at } = eventPromise; + // the admin has confirmed, delete the slot + Event.delete( + { id, mode: $scope.deleteMode }, + function (res) { + // delete success + if (res.deleted > 1) { + growl.success(_t( + 'app.public.events_show.events_deleted', + {COUNT: res.deleted - 1} + )); + } else { + growl.success(_t( + 'app.public.events_show.event_successfully_deleted' + )); + } + $uibModalInstance.close({ + status: 'success', + events: res.details.map(function (d) { return d.event.id }) + }); + }, + function (res) { + // not everything was deleted + const { data } = res; + if (data.total > 1) { + growl.warning(_t( + 'app.public.events_show.events_not_deleted', + {TOTAL: data.total, COUNT: data.total - data.deleted} + )); + } else { + growl.error(_t( + 'app.public.events_show.unable_to_delete_the_event' + )); + } + $uibModalInstance.close({ + status: 'failed', + availabilities: data.details.filter(function (d) { return d.status }).map(function (d) { return d.event.id }) + }); + }); + } + + /** + * Cancellation callback + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + } + } +]); + diff --git a/app/assets/templates/events/deleteRecurrent.html b/app/assets/templates/events/deleteRecurrent.html new file mode 100644 index 000000000..9ed1a1847 --- /dev/null +++ b/app/assets/templates/events/deleteRecurrent.html @@ -0,0 +1,26 @@ + + + diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index 532db2f7f..0e6a7fc08 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -74,10 +74,11 @@ class API::EventsController < API::ApiController def destroy authorize Event - if @event.safe_destroy - head :no_content + res = EventService.delete(params[:id], params[:mode]) + if res.all? { |r| r[:status] } + render json: { deleted: res.length, details: res }, status: :ok else - head :unprocessable_entity + render json: { total: res.length, deleted: res.select { |r| r[:status] }.length, details: res }, status: :unprocessable_entity end end diff --git a/app/services/event_service.rb b/app/services/event_service.rb index 900605a5e..196acaeb6 100644 --- a/app/services/event_service.rb +++ b/app/services/event_service.rb @@ -41,4 +41,35 @@ class EventService end { start_at: start_at, end_at: end_at } end + + # delete one or more events (if periodic) + def self.delete(event_id, mode = 'single') + results = [] + event = Event.find(event_id) + events = case mode + when 'single' + [event] + when 'next' + Event.includes(:availability) + .where( + 'availabilities.start_at >= ? AND events.recurrence_id = ?', + event.availability.start_at, + event.recurrence_id + ) + .references(:availabilities, :events) + when 'all' + Event.where( + 'recurrence_id = ?', + event.recurrence_id + ) + else + [] + end + + events.each do |e| + # here we use double negation because safe_destroy can return either a boolean (false) or an Availability (in case of delete success) + results.push status: !!e.safe_destroy, event: e # rubocop:disable Style/DoubleNegation + end + results + end end diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index f56147ce4..60a396ccb 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -117,7 +117,6 @@ en: # confirmation modal you_will_receive_confirmation_instructions_by_email: You will receive confirmation instructions by email. - # forgotten password modal your_email_address_is_unknown: "Your e-mail address is unknown." you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password: "You will receive in a moment, an e-mail with instructions to reset your password." @@ -324,8 +323,14 @@ en: you_can_shift_this_reservation_on_the_following_slots: "You can shift this reservation on the following slots:" confirmation_required: "Confirmation required" do_you_really_want_to_delete_this_event: "Do you really want to delete this event?" + delete_recurring_event: "You're about to delete a periodic event. What do you want to do?" + delete_this_event: "Only this event" + delete_this_and_next: "This event and the following" + delete_all: "All events" event_successfully_deleted: "Event successfully deleted" - unable_to_delete_the_event_because_some_users_alredy_booked_it: "Unable to delete this event, it may have been already reserved by some users." + events_deleted: "The event, and {COUNT, plural, =1{one other} other{{COUNT} others}}, have been deleted" + unable_to_delete_the_event: "Unable to delete the event, it may be booked by a member" + events_not_deleted: "On {TOTAL} events, {COUNT, plural, =1{one was not deleted} other{{COUNT} were not deleted}}. Some reservations may exists on {COUNT, plural, =1{it} other{them}}." cancel_the_reservation: "Cancel the reservation" do_you_really_want_to_cancel_this_reservation_this_apply_to_all_booked_tickets: "Do you really want to cancel this reservation? This apply to ALL booked tickets." reservation_was_successfully_cancelled: "Reservation was successfully cancelled" diff --git a/config/locales/app.public.es.yml b/config/locales/app.public.es.yml index ff6cb8071..23f75de38 100644 --- a/config/locales/app.public.es.yml +++ b/config/locales/app.public.es.yml @@ -323,8 +323,14 @@ es: you_can_shift_this_reservation_on_the_following_slots: "Puede cambiar la reserva en los siguientes campos:" confirmation_required: "Confirmation required" do_you_really_want_to_delete_this_event: "Do you really want to delete this event?" + delete_recurring_event: "You're about to delete a periodic event. What do you want to do?" + delete_this_event: "Only this event" + delete_this_and_next: "This event and the following" + delete_all: "All events" event_successfully_deleted: "Event successfully deleted" - unable_to_delete_the_event_because_some_users_alredy_booked_it: "Unable to delete this event, it may have been already reserved by some users." + events_deleted: "The event, and {COUNT, plural, =1{one other} other{{COUNT} others}}, have been deleted" + unable_to_delete_the_event: "Unable to delete the event, it may be booked by a member" + events_not_deleted: "On {TOTAL} events, {COUNT, plural, =1{one was not deleted} other{{COUNT} were not deleted}}. Some reservations may exists on {COUNT, plural, =1{it} other{them}}." cancel_the_reservation: "Cancel the reservation" do_you_really_want_to_cancel_this_reservation_this_apply_to_all_booked_tickets: "Do you really want to cancel this reservation? This apply to ALL booked tickets." reservation_was_successfully_cancelled: "Reservation was successfully cancelled" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 95f43f760..f2d708612 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -323,8 +323,14 @@ fr: you_can_shift_this_reservation_on_the_following_slots: "Vous pouvez déplacer cette réservation sur les créneaux suivants :" confirmation_required: "Confirmation requise" do_you_really_want_to_delete_this_event: "Voulez-vous vraiment supprimer cet évènement ?" + delete_recurring_event: "Vous êtes sur le point de supprimer un évènement périodique. Que voulez-vous supprimer ?" + delete_this_event: "Uniquement cet évènement" + delete_this_and_next: "Cet évènement et tous les suivants" + delete_all: "Tous les évènements" event_successfully_deleted: "L'évènement a bien été supprimé." - unable_to_delete_the_event_because_some_users_alredy_booked_it: "Impossible de supprimer l'évènement, il est peut-être déjà réservé par certains utilisateurs." + events_deleted: "L'évènement, ainsi {COUNT, plural, =1{qu'un autre} other{que {COUNT} autres}}, ont été supprimés" + unable_to_delete_the_event: "L'évènement n'a pu être supprimé, probablement car il est déjà réservé par un membre" + events_not_deleted: "Sur {TOTAL} évènements, {COUNT, plural, =1{un n'a pas pu être supprimé} other{{COUNT} n'ont pas pu être supprimés}}. Il est possible que des réservations existent sur {COUNT, plural, =1{celui-ci} other{ceux-ci}}." cancel_the_reservation: "Annuler la réservation" do_you_really_want_to_cancel_this_reservation_this_apply_to_all_booked_tickets: "Êtes vous sur de vouloir annuler cette réservation? Ceci s'applique à TOUTES les places réservées." reservation_was_successfully_cancelled: "La réservation a bien été annulée." diff --git a/config/locales/app.public.pt.yml b/config/locales/app.public.pt.yml index 5e4302423..9eeb153b8 100755 --- a/config/locales/app.public.pt.yml +++ b/config/locales/app.public.pt.yml @@ -323,8 +323,14 @@ pt: you_can_shift_this_reservation_on_the_following_slots: "Você pode alterar essa reserva nos campos a seguir:" confirmation_required: "Confirmação obrigatória" do_you_really_want_to_delete_this_event: "Vocêrealmente deseja remover este evento?" + delete_recurring_event: "You're about to delete a periodic event. What do you want to do?" + delete_this_event: "Only this event" + delete_this_and_next: "This event and the following" + delete_all: "All events" event_successfully_deleted: "Evento excluído com sucesso" - unable_to_delete_the_event_because_some_users_alredy_booked_it: "Não foi possível excluir este evento, já pode ter sido reservado por alguns usuários." + events_deleted: "The event, and {COUNT, plural, =1{one other} other{{COUNT} others}}, have been deleted" + unable_to_delete_the_event: "Unable to delete the event, it may be booked by a member" + events_not_deleted: "On {TOTAL} events, {COUNT, plural, =1{one was not deleted} other{{COUNT} were not deleted}}. Some reservations may exists on {COUNT, plural, =1{it} other{them}}." cancel_the_reservation: "Cancel the reservation" do_you_really_want_to_cancel_this_reservation_this_apply_to_all_booked_tickets: "Do you really want to cancel this reservation? This apply to ALL booked tickets." reservation_was_successfully_cancelled: "Reservation was successfully cancelled"