diff --git a/src/backoffice/views/schedules.py b/src/backoffice/views/schedules.py index b4ffa0b2311124f5867551824970ba9f418c9986..b69c8792f9e4a24eac7fef4d0fb9dabf22c89f91 100644 --- a/src/backoffice/views/schedules.py +++ b/src/backoffice/views/schedules.py @@ -128,7 +128,7 @@ class ScheduleSourceImportDetailView(ScheduleAdminMixin, DetailView): if data := ctx['object'].data: ctx['activity'] = sorted( data.get('_activity', []), - key=lambda item: (action2sort.get(item['action'], 99), type2sort.get(item['type'], 99), item['source_id']), + key=lambda item: (action2sort.get(item['action'], 99), type2sort.get(item['type'], 99), str(item['source_id'])), ) else: ctx['activity'] = None diff --git a/src/core/models/schedules.py b/src/core/models/schedules.py index ad3ef19f8cf50bd4933bec45219e8b05b86d6864..d39c6087e179e5e1e983e23188b5a5f3585d6d22 100644 --- a/src/core/models/schedules.py +++ b/src/core/models/schedules.py @@ -206,51 +206,56 @@ class ScheduleSource(models.Model): return mapping, False except ObjectDoesNotExist: - lo = None - if create_local_object: - if mapping_type == ScheduleSourceMapping.MappingType.ROOM: - try: - if source_uuid: - lo = Room.objects.get(conference=self.conference, pk=source_uuid) - elif self.assembly: - lo = Room.objects.get(conference=self.conference, assembly=self.assembly, name__iexact=source_id) - else: - lo = Room.objects.get(conference=self.conference, name__iexact=source_id) - except Room.MultipleObjectsReturned: - raise ValueError('Room name is not unique, please provide room guid') - except Room.DoesNotExist: - if self.assembly: - lo = Room(conference=self.conference, pk=source_uuid, assembly=self.assembly) - else: - raise ValueError('Cannot create room for wildcard schedule source.') - - elif mapping_type == ScheduleSourceMapping.MappingType.EVENT: - assembly = self.assembly - if assembly is None: - # wildcard schedulesource: lookup assembly based on referenced room - if hints and 'room_lookup' in hints and 'room_name' in hints: - r = hints['room_lookup'](hints['room_name']) - assembly = r.assembly - + try: + # we could not find the mapping, but we have a source_uuid -> try to find the local object by that + mapping = self.mappings.get(mapping_type=mapping_type, local_id=source_uuid) + return mapping, False + except ObjectDoesNotExist: + lo = None + if create_local_object: + if mapping_type == ScheduleSourceMapping.MappingType.ROOM: + try: + if source_uuid: + lo = Room.objects.get(conference=self.conference, pk=source_uuid) + elif self.assembly: + lo = Room.objects.get(conference=self.conference, assembly=self.assembly, name__iexact=source_id) + else: + lo = Room.objects.get(conference=self.conference, name__iexact=source_id) + except Room.MultipleObjectsReturned: + raise ValueError('Room name is not unique, please provide room guid') + except Room.DoesNotExist: + if self.assembly: + lo = Room(conference=self.conference, pk=source_uuid, assembly=self.assembly) + else: + raise ValueError('Cannot create room for wildcard schedule source.') + + elif mapping_type == ScheduleSourceMapping.MappingType.EVENT: + assembly = self.assembly if assembly is None: - raise NotImplementedError('Event on wildcard schedulesource could not be matched to an assembly.') - - lo = Event(conference=self.conference, pk=source_uuid, assembly=assembly) - - elif mapping_type == ScheduleSourceMapping.MappingType.SPEAKER: - lo, _ = self._get_or_create_speaker( - mail_guid=source_uuid, - name=hints.get('name'), - addresses=hints.get('addresses'), - ) - - mapping = self.mappings.create( - mapping_type=mapping_type, - source_id=source_id, - local_id=lo.pk, - local_object=lo, - ) - return mapping, True + # wildcard schedulesource: lookup assembly based on referenced room + if hints and 'room_lookup' in hints and 'room_name' in hints: + r = hints['room_lookup'](hints['room_name']) + assembly = r.assembly + + if assembly is None: + raise NotImplementedError('Event on wildcard schedulesource could not be matched to an assembly.') + + lo = Event(conference=self.conference, pk=source_uuid, assembly=assembly) + + elif mapping_type == ScheduleSourceMapping.MappingType.SPEAKER: + lo, _ = self._get_or_create_speaker( + mail_guid=source_uuid, + name=hints.get('name'), + addresses=hints.get('addresses'), + ) + + mapping = self.mappings.create( + mapping_type=mapping_type, + source_id=source_id, + local_id=lo.pk, + local_object=lo, + ) + return mapping, True def _load_dataitem(self, activity: list, item: dict, item_source_id: str, item_type: str, expected_items: list, items: dict, from_dict_args: dict) -> str: if item_source_id is None: @@ -549,36 +554,41 @@ class ScheduleSource(models.Model): logging.exception('Import on ScheduleSource %s encountered exception on loading room "%s".', self.pk, r_id) # then load events - for e_id, e in data['events'].items(): - try: - if replace_conference_slug_prefix and e.get('slug', '').startswith(replace_conference_slug_prefix): - e['slug'] = self.conference.slug + e['slug'][len(replace_conference_slug_prefix) :] + for e_id, entries in data['events'].items(): + has_multiple_slots = isinstance(entries, list) + # `entries` can either by an single event of a group of events with the same id + for e in entries if has_multiple_slots else [entries]: + source_id = e.get('guid') if has_multiple_slots else e_id + try: + if replace_conference_slug_prefix and e.get('slug', '').startswith(replace_conference_slug_prefix): + e['slug'] = self.conference.slug + e['slug'][len(replace_conference_slug_prefix) :] + + self._load_dataitem( + activity=activity, + item=e, + item_source_id=source_id, + item_type='event', + expected_items=expected_events, + items=events, + from_dict_args={ + 'allow_kind': self.assembly.is_official if self.assembly else False, # TODO: lookup assembly's room if not given + 'allow_track': allow_track, # TODO + 'room_lookup': rooms.get, + 'speaker_lookup': speaker_lookup, + }, + ) - self._load_dataitem( - activity=activity, - item=e, - item_source_id=e_id, - item_type='event', - expected_items=expected_events, - items=events, - from_dict_args={ - 'allow_kind': self.assembly.is_official if self.assembly else False, # TODO: lookup assembly's room if not given - 'allow_track': allow_track, # TODO - 'room_lookup': rooms.get, - 'speaker_lookup': speaker_lookup, - }, - ) - except Exception as err: - activity.append( - { - 'action': 'error', - 'type': 'event', - 'source_id': e_id, - 'local_id': e.get('uuid', None), - 'message': str(err), - } - ) - logging.exception('Import on ScheduleSource %s encountered exception on loading event "%s".', self.pk, e_id) + except Exception as err: + activity.append( + { + 'action': 'error', + 'type': 'event', + 'source_id': source_id, + 'local_id': e.get('guid', None), + 'message': str(err), + } + ) + logging.exception('Import on ScheduleSource %s encountered exception on loading event "%s".', self.pk, e_id) # flag the non-loaded rooms as 'missing' for room_id in expected_rooms: diff --git a/src/core/schedules/schedulejson.py b/src/core/schedules/schedulejson.py index f6c92eaab1bd7d66db490e0ba30d121b2c68ce73..cb13339cc48f083859464b668ed48c4f46ca969b 100644 --- a/src/core/schedules/schedulejson.py +++ b/src/core/schedules/schedulejson.py @@ -69,31 +69,45 @@ class ScheduleJSONSupport(BaseScheduleSupport): return f'{host}{uri}' return uri + def format_event(e): + """ + transforms an raw ScheduleJSON event into a HUB event dict, which can ne used in Event.from_dict() + + :param e: event dict from ScheduleJSON class + :return: dict with data for Event.from_dict() + """ + return { + 'guid': e.get('guid'), + 'slug': e.get('slug').split(f"{e.get('id')}-")[1][0:150].strip('-') or e.get('slug')[0:150].strip('-'), + 'name': e.get('title'), + 'language': e.get('language'), + 'abstract': e.get('abstract') or '', + 'description': e.get('description') or '', + 'description_en': e.get('description') or '', + 'description_de': schedule_de.event(e.get('guid')).get('description') or '', + 'track': e.get('track'), + 'room': e.get('room'), + 'schedule_start': e.get('date'), + 'schedule_duration': str(schedule_time_to_timedelta(e.get('duration'))), + 'is_public': True, + 'kind': kind, + 'speakers': e.get('persons', []), + 'banner_image_url': ensure_full_url(e.get('logo')), + 'additional_data': filter_additional_data(e, self.computed_data(e), other_fields_to_drop), + } + + def format_group(events): + # if the submission has only one slot, we return a single event - to be backwards compatible + if len(events) == 1: + return format_event(events[0]) + + # if we have a submisson with multiple slots, we return a list of events + return [format_event(e) for e in events] + return { 'version': schedule.version(), 'rooms': {r['name']: r for r in schedule.rooms()}, - 'events': { - e.get('id'): { - 'guid': e.get('guid'), - 'slug': e.get('slug').split(f"{e.get('id')}-")[1][0:150].strip('-') or e.get('slug')[0:150].strip('-'), - 'name': e.get('title'), - 'language': e.get('language'), - 'abstract': e.get('abstract') or '', - 'description': e.get('description') or '', - 'description_en': e.get('description') or '', - 'description_de': schedule_de.event(e.get('guid')).get('description') or '', - 'track': e.get('track'), - 'room': e.get('room'), - 'schedule_start': e.get('date'), - 'schedule_duration': str(schedule_time_to_timedelta(e.get('duration'))), - 'is_public': True, - 'kind': kind, - 'speakers': e.get('persons', []), - 'banner_image_url': ensure_full_url(e.get('logo')), - 'additional_data': filter_additional_data(e, self.computed_data(e), other_fields_to_drop), - } - for e in schedule.events() - }, + 'events': {k: format_group(e) for k, e in schedule.events_grouped(by='id').items()}, } def computed_data(self, event: dict): @@ -171,6 +185,14 @@ class ScheduleJSON: for room in day.get('rooms'): yield from day.get('rooms')[room] + def events_grouped(self, by='id'): + groups = {} + for event in self.events(): + if event[by] not in groups: + groups[event[by]] = [] + groups[event[by]].append(event) + return groups + def event(self, guid): if guid in self._events: return self._events[guid]