def _convert_to_recurring_event(self, updates, original): """Convert a single event to a series of recurring events""" # Convert the single event to a series of recurring events merged = copy.deepcopy(original) merged.update(updates) generated_events = generate_recurring_events(merged) # Remove the first element in the list (the current event being updated) # And update the start/end dates to be in line with the new recurring rules updated_event = generated_events.pop(0) updates['dates']['start'] = updated_event['dates']['start'] updates['dates']['end'] = updated_event['dates']['end'] # Create the new events and generate their history self.create(generated_events) app.on_inserted_events(generated_events) return generated_events
def _convert_to_recurring_event(self, updates, original): """Convert a single event to a series of recurring events""" self._validate_convert_to_recurring(updates, original) updates['recurrence_id'] = generate_guid(type=GUID_NEWSML) merged = copy.deepcopy(original) merged.update(updates) # Generated new events will be "draft" merged[ITEM_STATE] = WORKFLOW_STATE.DRAFT generated_events = generate_recurring_events(merged) updated_event = generated_events.pop(0) # Check to see if the first generated event is different from original # If yes, mark original as rescheduled with generated recurrence_id if updated_event['dates']['start'].date( ) != original['dates']['start'].date(): # Reschedule original event updates['update_method'] = UPDATE_SINGLE event_reschedule_service = get_resource_service( 'events_reschedule') updates['dates'] = updated_event['dates'] set_planning_schedule(updates) event_reschedule_service.update_single_event(updates, original) if updates.get('state') == WORKFLOW_STATE.RESCHEDULED: history_service = get_resource_service('events_history') history_service.on_reschedule(updates, original) else: # Original event falls as a part of the series # Remove the first element in the list (the current event being updated) # And update the start/end dates to be in line with the new recurring rules updates['dates']['start'] = updated_event['dates']['start'] updates['dates']['end'] = updated_event['dates']['end'] set_planning_schedule(updates) remove_lock_information(item=updates) # Create the new events and generate their history self.create(generated_events) app.on_inserted_events(generated_events) return generated_events
def update_recurring_events(self, updates, original, update_method): remove_lock_information(updates) rules_changed = updates['dates']['recurring_rule'] != original['dates']['recurring_rule'] times_changed = updates['dates']['start'] != original['dates']['start'] or \ updates['dates']['end'] != original['dates']['end'] reason = updates.pop('reason', None) events_service = get_resource_service('events') historic, past, future = self.get_recurring_timeline(original, postponed=True) # Determine if the selected event is the first one, if so then # act as if we're changing future events if len(historic) == 0 and len(past) == 0: update_method = UPDATE_FUTURE if update_method == UPDATE_FUTURE: rescheduled_events = [original] + future new_start_date = updates['dates']['start'] original_start_date = original['dates']['start'] original_rule = original['dates']['recurring_rule'] else: rescheduled_events = past + [original] + future # Assign the date from the beginning of the new series new_start_date = updates['dates']['start'] original_start_date = past[0]['dates']['start'] original_rule = past[0]['dates']['recurring_rule'] updated_rule = deepcopy(updates['dates']['recurring_rule']) if updated_rule['endRepeatMode'] == 'count': num_events = len(historic) + len(past) + len(future) + 1 updated_rule['count'] -= num_events - len(rescheduled_events) # Compute the difference between start and end in the updated event time_delta = updates['dates']['end'] - updates['dates']['start'] # Generate the dates for the new event series new_dates = [date for date in islice(generate_recurring_dates( start=new_start_date, tz=updates['dates'].get('tz') and pytz.timezone(updates['dates']['tz'] or None), date_only=True, **updated_rule ), 0, 200)] # Generate the dates for the original events original_dates = [date for date in islice(generate_recurring_dates( start=original_start_date, tz=original['dates'].get('tz') and pytz.timezone(original['dates']['tz'] or None), date_only=True, **original_rule ), 0, 200)] self.set_next_occurrence(updates) dates_processed = [] # Iterate over the current events in the series and delete/spike # or update the event accordingly deleted_events = {} for event in rescheduled_events: if event[config.ID_FIELD] == original[config.ID_FIELD]: event_date = updates['dates']['start'].replace(tzinfo=None).date() else: event_date = event['dates']['start'].replace(tzinfo=None).date() # If the event does not occur in the new dates, then we need to either # delete or spike this event if event_date not in new_dates: # Add it to the list of events to delete or spike # This is done later so that we can perform a single # query against mongo, rather than one per deleted event deleted_events[event[config.ID_FIELD]] = event # If the date has already been processed, then we should mark this event for deletion # This occurs when the selected Event is being updated to an Event that already exists # in another Event in the series. # This stops multiple Events to occur on the same day elif event_date in new_dates and event_date in dates_processed: deleted_events[event[config.ID_FIELD]] = event # Otherwise this Event does occur in the new dates else: # Because this Event occurs in the new dates, then we are not to set the state to 'rescheduled', # instead we set it to either 'scheduled' (if public) or 'draft' (if not public) new_state = WORKFLOW_STATE.SCHEDULED if event.get('pubstatus') else WORKFLOW_STATE.DRAFT # If this is the selected Event, then simply update the fields and # Reschedule associated Planning items if event[config.ID_FIELD] == original[config.ID_FIELD]: self._mark_event_rescheduled(updates, original, reason, True) updates['state'] = new_state self._reschedule_event_plannings(event, reason, state=WORKFLOW_STATE.DRAFT) else: new_updates = { 'reason': reason, 'skip_on_update': True } self._mark_event_rescheduled(new_updates, event, reason) new_updates['state'] = new_state # Update the 'start', 'end' and 'recurring_rule' fields of the Event if rules_changed or times_changed: new_updates['state'] = new_state new_updates['dates'] = event['dates'] new_updates['dates']['start'] = datetime.combine(event_date, updates['dates']['start'].time()) new_updates['dates']['end'] = new_updates['dates']['start'] + time_delta new_updates['dates']['recurring_rule'] = updates['dates']['recurring_rule'] self.set_planning_schedule(new_updates) # And finally update the Event, and Reschedule associated Planning items self.patch(event[config.ID_FIELD], new_updates) self._reschedule_event_plannings(event, reason, state=WORKFLOW_STATE.DRAFT) app.on_updated_events_reschedule(new_updates, {'_id': event[config.ID_FIELD]}) # Mark this date as being already processed dates_processed.append(event_date) # Create new events that do not fall on the original occurrence dates new_events = [] for date in new_dates: # If the new date falls on the original occurrences, or if the # start date of the selected one, then skip this date occurrence if date in original_dates or date in dates_processed: continue # Create a copy of the metadata to use for the new event new_event = deepcopy(original) new_event.update(deepcopy(updates)) # Remove fields not required by the new events for key in list(new_event.keys()): if key.startswith('_'): new_event.pop(key) elif key.startswith('lock_'): new_event.pop(key) # Set the new start and end dates, as well as the _id and guid fields new_event['dates']['start'] = datetime.combine(date, updates['dates']['start'].time()) new_event['dates']['end'] = new_event['dates']['start'] + time_delta new_event[config.ID_FIELD] = new_event['guid'] = generate_guid(type=GUID_NEWSML) new_event.pop('reason', None) self.set_planning_schedule(new_event) # And finally add this event to the list of events to be created new_events.append(new_event) # Now iterate over the new events and create them if new_events: events_service.create(new_events) app.on_inserted_events(new_events) # Iterate over the events to delete/spike self._set_events_planning(deleted_events) for event in deleted_events.values(): event_plans = event.get('_plans', []) is_original = event[config.ID_FIELD] == original[config.ID_FIELD] if len(event_plans) > 0 or event.get('pubstatus', None) is not None: if is_original: self._mark_event_rescheduled(updates, original, reason) else: # This event has Planning items, so spike this event and # all Planning items new_updates = { 'skip_on_update': True, 'reason': reason } self._mark_event_rescheduled(new_updates, original, reason) self.patch(event[config.ID_FIELD], new_updates) if len(event_plans) > 0: self._reschedule_event_plannings(original, reason, event_plans) else: # This event has no Planning items, therefor we can safely # delete this event events_service.delete_action(lookup={'_id': event[config.ID_FIELD]}) app.on_deleted_item_events(event) if is_original: updates['_deleted'] = True
def _reschedule_recurring_events(self, updates, original, update_method, events_service): original_deleted = False rules_changed = updates['dates']['recurring_rule'] != original[ 'dates']['recurring_rule'] # Release the Lock on the selected Event updates.update({ LOCK_USER: None, LOCK_SESSION: None, 'lock_time': None, 'lock_action': None }) historic, past, future = events_service.get_recurring_timeline( original) # Determine if the selected event is the first one, if so then # act as if we're changing future events if len(historic) == 0 and len(past) == 0: update_method = UPDATE_FUTURE if update_method == UPDATE_FUTURE: rescheduled_events = [original] + future new_start_date = updates['dates']['start'] original_start_date = original['dates']['start'] original_rule = original['dates']['recurring_rule'] else: rescheduled_events = past + [original] + future # Assign the date from the beginning of the new series new_start_date = past[0]['dates']['start'] + \ (updates['dates']['start'] - original['dates']['start']) original_start_date = past[0]['dates']['start'] original_rule = past[0]['dates']['recurring_rule'] updated_rule = deepcopy(updates['dates']['recurring_rule']) if updated_rule['endRepeatMode'] == 'count': num_events = len(historic) + len(past) + len(future) + 1 updated_rule['count'] -= num_events - len(rescheduled_events) # Compute the difference between start and end in the updated event time_delta = updates['dates']['end'] - updates['dates']['start'] # Generate the dates for the new event series new_dates = [ date for date in islice( generate_recurring_dates( start=new_start_date, tz=updates['dates'].get('tz') and pytz.timezone(updates['dates']['tz'] or None), **updated_rule), 0, 200) ] # Generate the dates for the original events original_dates = [ date for date in islice( generate_recurring_dates( start=original_start_date, tz=original['dates'].get('tz') and pytz.timezone(original['dates']['tz'] or None), **original_rule), 0, 200) ] set_next_occurrence(updates) # Iterate over the current events in the series and delete/spike # or update the event accordingly deleted_events = {} for event in rescheduled_events: # If the event does not occur in the new dates, then we need to either # delete or spike this event if event['dates']['start'].replace(tzinfo=None) not in new_dates: # Add it to the list of events to delete or spike # This is done later so that we can perform a single # query against mongo, rather than one per deleted event deleted_events[event[config.ID_FIELD]] = event # Otherwise if this Event does occur in the new dates, and the recurring rules have # changed, then make sure this event has the new recurring rules applied. # This can occur when extending a series where the original Events are kept and only # new events are created. elif rules_changed and event[config.ID_FIELD] != original[ config.ID_FIELD]: new_updates = {'dates': event['dates']} new_updates['dates']['recurring_rule'] = updates['dates'][ 'recurring_rule'] events_service.patch(event[config.ID_FIELD], new_updates) app.on_updated_events_reschedule( new_updates, {'_id': event[config.ID_FIELD]}) # Create new events that do not fall on the original occurrence dates new_events = [] for date in new_dates: # If the new date falls on the original occurrences, or is the # start date of the selected one, then skip this date occurrence if date in original_dates: continue # Create a copy of the metadata to use for the new event new_event = deepcopy(original) new_event.update(deepcopy(updates)) # Remove fields not required by the new events for key in list(new_event.keys()): if key.startswith('_'): new_event.pop(key) elif key.startswith('lock_'): new_event.pop(key) # Set the new start and end dates, as well as the _id and guid fields new_event['dates']['start'] = date new_event['dates']['end'] = date + time_delta new_event[config.ID_FIELD] = new_event['guid'] = generate_guid( type=GUID_NEWSML) new_event.pop('reason', None) # And finally add this event to the list of events to be created new_events.append(new_event) # Now iterate over the new events and create them if new_events: events_service.create(new_events) app.on_inserted_events(new_events) # Iterate over the events to delete/spike self._set_events_planning(deleted_events) for event in deleted_events.values(): event_plans = event.get('_plans', []) is_original = event[config.ID_FIELD] == original[config.ID_FIELD] if len(event_plans) > 0 or event.get('pubstatus', None) is not None: if is_original: self._mark_event_rescheduled(updates, original) else: # This event has Planning items, so spike this event and # all Planning items new_updates = { 'skip_on_update': True, 'reason': updates.get('reason', None) } self._mark_event_rescheduled(new_updates, original) self.patch(event[config.ID_FIELD], new_updates) if len(event_plans) > 0: self._reschedule_event_plannings(updates, original, event_plans) else: # This event has no Planning items, therefor we can safely # delete this event events_service.delete_action( lookup={'_id': event[config.ID_FIELD]}) app.on_deleted_item_events(event) if is_original: original_deleted = True return not original_deleted
def _update_recurring_events(self, updates, original): """Method to update recurring events. If the recurring_rule has been removed for this event, process it separately, otherwise update the event and/or its recurring rules """ merged = copy.deepcopy(original) merged.update(updates) if updates.get('dates'): if not updates['dates'].get('recurring_rule', None): # Recurring rule has been removed for this event, # Remove this rule and return from this method self._remove_recurring_rules(updates, original) push_notification( 'events:updated', item=str(original[config.ID_FIELD]), user=str(updates.get('version_creator')) ) return # Generate the list of changes to the series of events based on the # new recurring_rules new_events, updated_events, deleted_events = self._update_recurring_rules(updates, original) else: # We only update events here, so get the list of future events updated_events = self._get_future_events(merged) new_events = deleted_events = [] # Create instances for the new events, and save them to mongo/elastic # Then fire off events for them added_events = [] for e in new_events: event = copy.deepcopy(merged) event['dates']['start'] = e['dates']['start'] event['dates']['end'] = e['dates']['end'] event['_id'] = event['guid'] = generate_guid(type=GUID_NEWSML) added_events.append(event) if added_events: self.create(added_events) app.on_inserted_events(added_events) # For all the deleted events, remove them from mongo/elastic # Then fire off events for them for e in deleted_events: self.delete({'_id': e[config.ID_FIELD]}) app.on_deleted_item_events(e) # For all the updated events, update their dates/metadata # Then fire off events for them for e in updated_events: if e[config.ID_FIELD] == original[config.ID_FIELD]: continue updated_event = copy.deepcopy(updates) if 'dates' not in updated_event: updated_event['dates'] = original['dates'] if 'guid' in updated_event: del updated_event['guid'] updated_event['dates']['start'] = e['dates']['start'] updated_event['dates']['end'] = e['dates']['end'] updated_event['skip_on_update'] = True self.patch(e[config.ID_FIELD], updated_event) app.on_updated_events(updated_event, {'_id': e[config.ID_FIELD]}) # And finally push a notification to connected clients push_notification( 'events:updated:recurring', item=str(original[config.ID_FIELD]), recurrence_id=str(original['recurrence_id']), user=str(updates.get('version_creator', '')) )