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"