From 851b553066bdf3a3632689d4750e271b05296242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Avil=C3=A9s?= Date: Fri, 22 Dec 2023 14:44:28 +0100 Subject: [PATCH] Link objects to reservation occurrences --- ..._link_events_to_reservation_occurrences.py | 121 ++++++++++++++++++ .../categories/controllers/management.py | 17 ++- indico/modules/events/api.py | 14 -- indico/modules/events/client/js/creation.js | 1 - .../contributions/models/contributions.py | 2 +- .../events/management/controllers/settings.py | 2 +- .../management/templates/delete_event.html | 4 +- .../management/templates/delete_events.html | 4 +- indico/modules/events/models/events.py | 8 +- .../modules/events/sessions/models/blocks.py | 2 +- indico/modules/rb/__init__.py | 18 +-- .../rb/controllers/backend/bookings.py | 3 +- indico/modules/rb/event/controllers.py | 12 +- .../rb/models/reservation_occurrences.py | 53 ++++++++ indico/modules/rb/models/reservations.py | 52 -------- indico/modules/rb/operations/bookings.py | 18 ++- indico/modules/rb/schemas.py | 16 +-- indico/modules/rb/templates/booking_list.html | 24 ++-- 18 files changed, 232 insertions(+), 139 deletions(-) create mode 100644 indico/migrations/versions/20231222_1408_b37cbc4bb129_link_events_to_reservation_occurrences.py diff --git a/indico/migrations/versions/20231222_1408_b37cbc4bb129_link_events_to_reservation_occurrences.py b/indico/migrations/versions/20231222_1408_b37cbc4bb129_link_events_to_reservation_occurrences.py new file mode 100644 index 00000000000..4dda55187e9 --- /dev/null +++ b/indico/migrations/versions/20231222_1408_b37cbc4bb129_link_events_to_reservation_occurrences.py @@ -0,0 +1,121 @@ +"""Link events to reservation occurrences. + +Revision ID: b37cbc4bb129 +Revises: e2b69fe5155d +Create Date: 2023-12-22 14:08:45.669303 +""" + +from enum import Enum + +import sqlalchemy as sa +from alembic import op + +from indico.core.db.sqlalchemy import PyIntEnum + + +# revision identifiers, used by Alembic. +revision = 'b37cbc4bb129' +down_revision = 'e2b69fe5155d' +branch_labels = None +depends_on = None + + +class _LinkType(int, Enum): + category = 1 + event = 2 + contribution = 3 + subcontribution = 4 + session = 5 + session_block = 6 + + +excluded_link_types = {_LinkType.category, _LinkType.subcontribution, _LinkType.session} + + +def upgrade(): + # Create reservation occurrence links + op.create_table( + 'reservation_occurrence_links', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('link_type', PyIntEnum(_LinkType, exclude_values=excluded_link_types), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=True), + sa.Column('linked_event_id', sa.Integer(), nullable=True), + sa.Column('session_block_id', sa.Integer(), nullable=True), + sa.Column('contribution_id', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['contribution_id'], ['events.contributions.id']), + sa.ForeignKeyConstraint(['event_id'], ['events.events.id']), + sa.ForeignKeyConstraint(['linked_event_id'], ['events.events.id']), + sa.ForeignKeyConstraint(['session_block_id'], ['events.session_blocks.id']), + sa.CheckConstraint('(event_id IS NULL) = (link_type = 1)', name='valid_event_id'), + sa.CheckConstraint('link_type != 2 ' + 'OR (contribution_id IS NULL AND session_block_id IS NULL AND linked_event_id IS NOT NULL)', + name='valid_event_link'), + sa.CheckConstraint('link_type != 3 ' + 'OR (linked_event_id IS NULL AND session_block_id IS NULL AND contribution_id IS NOT NULL)', + name='valid_contribution_link'), + sa.CheckConstraint('link_type != 6 ' + 'OR (contribution_id IS NULL AND linked_event_id IS NULL AND session_block_id IS NOT NULL)', + name='valid_session_block_link'), + schema='roombooking' + ) + op.add_column('reservation_occurrences', sa.Column('link_id', sa.Integer(), nullable=True), schema='roombooking') + op.create_foreign_key(None, 'reservation_occurrences', 'reservation_occurrence_links', ['link_id'], ['id'], + source_schema='roombooking', referent_schema='roombooking') + op.create_index(None, 'reservation_occurrence_links', ['contribution_id'], unique=False, schema='roombooking') + op.create_index(None, 'reservation_occurrence_links', ['event_id'], unique=False, schema='roombooking') + op.create_index(None, 'reservation_occurrence_links', ['linked_event_id'], unique=False, schema='roombooking') + op.create_index(None, 'reservation_occurrence_links', ['session_block_id'], unique=False, schema='roombooking') + op.create_index(None, 'reservation_occurrences', ['link_id'], unique=False, schema='roombooking') + + # Migrate data + # TODO + + # Drop reservation links + op.drop_index('ix_reservations_link_id', table_name='reservations', schema='roombooking') + op.drop_constraint('fk_reservations_link_id_reservation_links', 'reservations', schema='roombooking') + op.drop_table('reservation_links', schema='roombooking') + op.drop_column('reservations', 'link_id', schema='roombooking') + + +def downgrade(): + # Recreate reservation links + op.add_column('reservations', sa.Column('link_id', sa.INTEGER(), nullable=True), + schema='roombooking') + op.create_foreign_key(None, 'reservations', 'reservation_links', ['link_id'], ['id'], + source_schema='roombooking', referent_schema='roombooking') + op.create_table( + 'reservation_links', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('link_type', sa.SMALLINT(), nullable=False), + sa.Column('event_id', sa.INTEGER(), nullable=True), + sa.Column('linked_event_id', sa.INTEGER(), nullable=True), + sa.Column('session_block_id', sa.INTEGER(), nullable=True), + sa.Column('contribution_id', sa.INTEGER(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['contribution_id'], ['events.contributions.id']), + sa.ForeignKeyConstraint(['event_id'], ['events.events.id']), + sa.ForeignKeyConstraint(['linked_event_id'], ['events.events.id']), + sa.ForeignKeyConstraint(['session_block_id'], ['events.session_blocks.id']), + sa.CheckConstraint('(event_id IS NULL) = (link_type = 1)', name='valid_event_id'), + sa.CheckConstraint('(link_type <> 2) ' + 'OR (contribution_id IS NULL AND session_block_id IS NULL AND linked_event_id IS NOT NULL)', + name='valid_event_link'), + sa.CheckConstraint('(link_type <> 3) ' + 'OR (linked_event_id IS NULL AND session_block_id IS NULL AND contribution_id IS NOT NULL)', + name='valid_contribution_link'), + sa.CheckConstraint('(link_type <> 6) ' + 'OR (contribution_id IS NULL AND linked_event_id IS NULL AND session_block_id IS NOT NULL)', + name='valid_session_block_link'), + schema='roombooking' + ) + + # Migrate data + # TODO + + # Drop reservation occurrence links + op.drop_index('ix_reservation_occurrences_link_id', table_name='reservation_occurrences', schema='roombooking') + op.drop_constraint('fk_reservation_occurrences_link_id_reservation_occurrence_links', 'reservation_occurrences', + schema='roombooking') + op.drop_column('reservation_occurrences', 'link_id', schema='roombooking') + op.drop_table('reservation_occurrence_links', schema='roombooking') diff --git a/indico/modules/categories/controllers/management.py b/indico/modules/categories/controllers/management.py index 8af7cf9a376..a6a3bb768d5 100644 --- a/indico/modules/categories/controllers/management.py +++ b/indico/modules/categories/controllers/management.py @@ -36,7 +36,7 @@ from indico.modules.events.notifications import notify_move_request_closure, notify_move_request_creation from indico.modules.events.operations import create_event_request from indico.modules.logs.models.entries import CategoryLogRealm, LogKind -from indico.modules.rb.models.reservations import Reservation, ReservationLink +from indico.modules.rb.models.reservation_occurrences import ReservationOccurrence, ReservationOccurrenceLink from indico.modules.users import User from indico.util.fs import secure_filename from indico.util.i18n import _, ngettext @@ -422,15 +422,14 @@ class RHDeleteEvents(RHManageCategorySelectedEventsBase): def _process(self): is_submitted = 'confirmed' in request.form if not is_submitted: - # find out which active bookings are linked to the events in question - num_bookings = (Reservation.query - .join(ReservationLink) - .join(Event, Event.id == ReservationLink.linked_event_id) - .filter(Event.id.in_(e.id for e in self.events), - Reservation.is_pending | Reservation.is_accepted) - .count()) + # find out which active booking occurrences are linked to the events in question + num_booking_occurrences = (ReservationOccurrence.query + .join(ReservationOccurrenceLink) + .join(Event, Event.id == ReservationOccurrenceLink.linked_event_id) + .filter(Event.id.in_(e.id for e in self.events), ReservationOccurrence.is_valid) + .count()) return jsonify_template('events/management/delete_events.html', - events=self.events, num_bookings=num_bookings) + events=self.events, num_booking_occurrences=num_booking_occurrences) for ev in self.events[:]: ev.delete('Bulk-deleted by category manager', session.user) flash(ngettext('You have deleted one event', 'You have deleted {} events', len(self.events)) diff --git a/indico/modules/events/api.py b/indico/modules/events/api.py index a0b24bf65f6..b10dbbde58d 100644 --- a/indico/modules/events/api.py +++ b/indico/modules/events/api.py @@ -7,7 +7,6 @@ import fnmatch import re -from collections import defaultdict from datetime import datetime from hashlib import md5 from operator import attrgetter @@ -35,7 +34,6 @@ from indico.modules.events.sessions.models.sessions import Session from indico.modules.events.timetable.legacy import TimetableSerializer from indico.modules.events.timetable.models.entries import TimetableEntry -from indico.modules.rb.models.reservation_occurrences import ReservationOccurrence from indico.util.date_time import iterdays from indico.util.i18n import orig_string from indico.util.signals import values_from_signal @@ -281,24 +279,12 @@ def _build_event_api_data_base(self, event): 'references': [self.serialize_reference(x) for x in event.references] } - def _serialize_reservations(self, reservations): - res = defaultdict(list) - for resv in reservations: - occurrences = (resv.occurrences - .filter(ReservationOccurrence.is_valid) - .options(ReservationOccurrence.NO_RESERVATION_USER_STRATEGY) - .all()) - res[resv.room.full_name] += [{'startDateTime': occ.start_dt, 'endDateTime': occ.end_dt} - for occ in occurrences] - return res - def _build_session_event_api_data(self, event): data = self._build_event_api_data_base(event) data.update({ '_fossil': 'conference', 'adjustedStartDate': self._serialize_date(event.start_dt_local), 'adjustedEndDate': self._serialize_date(event.end_dt_local), - 'bookedRooms': self._serialize_reservations(event.reservations), 'supportInfo': { '_fossil': 'supportInfo', 'caption': event.contact_title, diff --git a/indico/modules/events/client/js/creation.js b/indico/modules/events/client/js/creation.js index 9e6f06701c6..40ae76df65f 100644 --- a/indico/modules/events/client/js/creation.js +++ b/indico/modules/events/client/js/creation.js @@ -207,7 +207,6 @@ import {camelizeKeys} from 'indico/utils/case'; !('room_id' in roomData) || !startDt.isValid() || !endDt.isValid() || - !startDt.isSame(endDt, 'day') || startDt.isSameOrAfter(endDt) || isCategoryExcluded(category['id']) || timezone !== options.serverDefaultTimezone || diff --git a/indico/modules/events/contributions/models/contributions.py b/indico/modules/events/contributions/models/contributions.py index bcffe2b7dd6..a24a0e87acf 100644 --- a/indico/modules/events/contributions/models/contributions.py +++ b/indico/modules/events/contributions/models/contributions.py @@ -371,7 +371,7 @@ def _paper_last_revision(cls): # - editables (Editable.contribution) # - legacy_mapping (LegacyContributionMapping.contribution) # - note (EventNote.contribution) - # - room_reservation_links (ReservationLink.contribution) + # - room_reservation_occurrence_links (ReservationOccurrenceLink.contribution) # - timetable_entry (TimetableEntry.contribution) # - vc_room_associations (VCRoomEventAssociation.linked_contrib) diff --git a/indico/modules/events/management/controllers/settings.py b/indico/modules/events/management/controllers/settings.py index 9216951ab24..1b4c128b50f 100644 --- a/indico/modules/events/management/controllers/settings.py +++ b/indico/modules/events/management/controllers/settings.py @@ -51,7 +51,7 @@ def _check_access(self): def _process(self): show_booking_warning = False if (config.ENABLE_ROOMBOOKING and rb_check_if_visible(session.user) - and not self.event.has_ended and self.event.room and not self.event.room_reservation_links): + and not self.event.has_ended and self.event.room and not self.event.room_reservation_occurrence_links): # Check if any of the managers of the event already have a booking that overlaps with the event datetime manager_ids = [p.user.id for p in self.event.acl_entries if p.user] has_overlap = (ReservationOccurrence.query diff --git a/indico/modules/events/management/templates/delete_event.html b/indico/modules/events/management/templates/delete_event.html index d3c423b987a..c0aa5f7b031 100644 --- a/indico/modules/events/management/templates/delete_event.html +++ b/indico/modules/events/management/templates/delete_event.html @@ -1,11 +1,11 @@ {% from 'confirmation_dialog.html' import confirmation_dialog %} -{% set num_bookings = event.all_room_reservation_links.count() %} +{% set num_bookings = event.all_room_reservation_occurrence_links.count() %} {% set confirmation_message %} {% if num_bookings %} {% trans strong=''|safe, endstrong=''|safe -%} Please note that if you delete the event you will lose all the information contained in it, - {{ strong }}including {{ num_bookings }} Room Booking(s){{ endstrong }} linked to it. + {{ strong }}including {{ num_bookings }} Room Booking Occurrences(s){{ endstrong }} linked to it. This operation is irreversible! {%- endtrans %} {% else %} diff --git a/indico/modules/events/management/templates/delete_events.html b/indico/modules/events/management/templates/delete_events.html index 0ccf599ea55..5196c6740f6 100644 --- a/indico/modules/events/management/templates/delete_events.html +++ b/indico/modules/events/management/templates/delete_events.html @@ -3,10 +3,10 @@ {% set ok_text=_("Delete events") %} {% set confirmation_message %} - {% if num_bookings %} + {% if num_booking_occurrences %} {% trans strong=''|safe, endstrong=''|safe -%} Please note that if you delete the events you will lose all the information contained in them, - {{ strong }}including {{ num_bookings }} Room Booking(s){{ endstrong }}. + {{ strong }}including {{ num_booking_occurrences }} Room Booking Occurrences(s){{ endstrong }}. This operation is irreversible! {%- endtrans %} {% else %} diff --git a/indico/modules/events/models/events.py b/indico/modules/events/models/events.py index 98f4798bcb1..5ff94c17d40 100644 --- a/indico/modules/events/models/events.py +++ b/indico/modules/events/models/events.py @@ -401,7 +401,7 @@ def __table_args__(cls): # - all_legacy_attachment_folder_mappings (LegacyAttachmentFolderMapping.event) # - all_legacy_attachment_mappings (LegacyAttachmentMapping.event) # - all_notes (EventNote.event) - # - all_room_reservation_links (ReservationLink.event) + # - all_room_reservation_occurrence_links (ReservationOccurrenceLink.event) # - all_vc_room_associations (VCRoomEventAssociation.event) # - attachment_folders (AttachmentFolder.linked_event) # - clones (Event.cloned_from) @@ -435,7 +435,7 @@ def __table_args__(cls): # - reminders (EventReminder.event) # - requests (Request.event) # - roles (EventRole.event) - # - room_reservation_links (ReservationLink.linked_event) + # - room_reservation_occurrence_links (ReservationOccurrenceLink.linked_event) # - session_types (SessionType.event) # - sessions (Session.event) # - settings (EventSetting.event) @@ -1044,10 +1044,6 @@ def cfp(self): from indico.modules.events.papers.models.call_for_papers import CallForPapers return CallForPapers(self) - @property - def reservations(self): - return [link.reservation for link in self.all_room_reservation_links] - @property def has_ended(self): return self.end_dt <= now_utc() diff --git a/indico/modules/events/sessions/models/blocks.py b/indico/modules/events/sessions/models/blocks.py index a9948a25d45..65cfcb84b4c 100644 --- a/indico/modules/events/sessions/models/blocks.py +++ b/indico/modules/events/sessions/models/blocks.py @@ -72,7 +72,7 @@ def __table_args__(cls): # relationship backrefs: # - contributions (Contribution.session_block) # - legacy_mapping (LegacySessionBlockMapping.session_block) - # - room_reservation_links (ReservationLink.session_block) + # - room_reservation_occurrence_links (ReservationOccurrenceLink.session_block) # - session (Session.blocks) # - timetable_entry (TimetableEntry.session_block) # - vc_room_associations (VCRoomEventAssociation.linked_block) diff --git a/indico/modules/rb/__init__.py b/indico/modules/rb/__init__.py index 0a6e453ecf9..705310c7509 100644 --- a/indico/modules/rb/__init__.py +++ b/indico/modules/rb/__init__.py @@ -80,7 +80,7 @@ def _topmenu_items(sender, **kwargs): @signals.menu.items.connect_via('event-management-sidemenu') def _sidemenu_items(sender, event, **kwargs): if config.ENABLE_ROOMBOOKING and event.can_manage(session.user): - yield SideMenuItem('room_booking', _('Room booking'), url_for('rb.event_booking_list', event), 50, + yield SideMenuItem('room_booking', _('Room bookings'), url_for('rb.event_booking_list', event), 50, icon='location') @@ -101,14 +101,14 @@ def _merge_users(target, source, **kwargs): @signals.event.deleted.connect def _event_deleted(event, user, **kwargs): - from indico.modules.rb.models.reservations import Reservation - reservation_links = (event.all_room_reservation_links - .join(Reservation) - .filter(~Reservation.is_rejected, ~Reservation.is_cancelled) - .all()) - for link in reservation_links: - if link.reservation.end_dt >= datetime.now(): - link.reservation.cancel(user or session.user, 'Associated event was deleted') + from indico.modules.rb.models.reservations import ReservationOccurrence + reservation_occurrence_links = (event.all_room_reservation_occurrence_links + .join(ReservationOccurrence) + .filter(~ReservationOccurrence.is_rejected, ~ReservationOccurrence.is_cancelled) + .all()) + for link in reservation_occurrence_links: + if link.reservation_occurrence.end_dt >= datetime.now(): + link.reservation_occurrence.cancel(user or session.user, 'Associated event was deleted') class BookPermission(ManagementPermission): diff --git a/indico/modules/rb/controllers/backend/bookings.py b/indico/modules/rb/controllers/backend/bookings.py index 7502868b74e..b60c987f48a 100644 --- a/indico/modules/rb/controllers/backend/bookings.py +++ b/indico/modules/rb/controllers/backend/bookings.py @@ -157,7 +157,8 @@ def _link_booking(self, booking, type_, id_, link_back): obj = get_linked_object(type_, id_) if obj is None or not obj.event.can_manage(session.user): return - booking.linked_object = obj + for occurrence in booking.occurrences: + occurrence.linked_object = obj if link_back: obj.inherit_location = False obj.room = self.room diff --git a/indico/modules/rb/event/controllers.py b/indico/modules/rb/event/controllers.py index 64ad1e00b96..ddeaba41430 100644 --- a/indico/modules/rb/event/controllers.py +++ b/indico/modules/rb/event/controllers.py @@ -16,7 +16,7 @@ from indico.modules.events.sessions.models.sessions import Session from indico.modules.events.timetable import TimetableEntry from indico.modules.rb.event.forms import BookingListForm -from indico.modules.rb.models.reservations import Reservation, ReservationLink +from indico.modules.rb.models.reservation_occurrences import ReservationOccurrence, ReservationOccurrenceLink from indico.modules.rb.util import get_booking_params_for_event, rb_check_if_visible from indico.modules.rb.views import WPEventBookingList from indico.util.date_time import format_datetime, now_utc @@ -52,13 +52,13 @@ def _process(self): has_contribs = _contrib_query(self.event).has_rows() has_session_blocks = _session_block_query(self.event).has_rows() - links = (ReservationLink.query.with_parent(self.event) - .options(joinedload('reservation').joinedload('room'), + links = (ReservationOccurrenceLink.query.with_parent(self.event) + .options(joinedload('reservation_occurrence').joinedload('reservation').joinedload('room'), joinedload('session_block'), joinedload('contribution')) - .filter(~ReservationLink.reservation.has(Reservation.is_cancelled)) - .join(Reservation) - .order_by(Reservation.start_dt) + .filter(~ReservationOccurrenceLink.reservation_occurrence.has(ReservationOccurrence.is_cancelled)) + .join(ReservationOccurrence) + .order_by(ReservationOccurrence.start_dt) .all()) contribs_data = {c.id: {'start_dt': c.start_dt.isoformat(), 'end_dt': c.end_dt.isoformat()} diff --git a/indico/modules/rb/models/reservation_occurrences.py b/indico/modules/rb/models/reservation_occurrences.py index 80bd087fb7c..6048c15a7ea 100644 --- a/indico/modules/rb/models/reservation_occurrences.py +++ b/indico/modules/rb/models/reservation_occurrences.py @@ -10,6 +10,7 @@ from dateutil import rrule from sqlalchemy import Date, or_ +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import contains_eager, defaultload from sqlalchemy.sql import cast @@ -17,6 +18,8 @@ from indico.core import signals from indico.core.db import db from indico.core.db.sqlalchemy import PyIntEnum +from indico.core.db.sqlalchemy.links import LinkMixin, LinkType +from indico.core.db.sqlalchemy.util.models import auto_table_args from indico.core.db.sqlalchemy.util.queries import db_dates_overlap from indico.core.errors import IndicoError from indico.modules.rb.models.reservation_edit_logs import ReservationEditLog @@ -36,6 +39,32 @@ class ReservationOccurrenceState(IndicoIntEnum): rejected = 4 +class ReservationOccurrenceLink(LinkMixin, db.Model): + __tablename__ = 'reservation_occurrence_links' + + @declared_attr + def __table_args__(cls): + return auto_table_args(cls, schema='roombooking') + + allowed_link_types = {LinkType.event, LinkType.contribution, LinkType.session_block} + events_backref_name = 'all_room_reservation_occurrence_links' + link_backref_name = 'room_reservation_occurrence_links' + + id = db.Column( + db.Integer, + primary_key=True + ) + + def __repr__(self): + return format_repr(self, 'id', _rawtext=self.link_repr) + + # relationship backrefs: + # - reservation_occurrence (ReservationOccurrence.link) + + +ReservationOccurrenceLink.register_link_events() + + class ReservationOccurrence(db.Model): __tablename__ = 'reservation_occurrences' __table_args__ = (db.CheckConstraint("rejection_reason != ''", 'rejection_reason_not_empty'), @@ -54,6 +83,12 @@ class ReservationOccurrence(db.Model): nullable=False, primary_key=True ) + link_id = db.Column( + db.Integer, + db.ForeignKey('roombooking.reservation_occurrence_links.id'), + nullable=True, + index=True + ) start_dt = db.Column( db.DateTime, nullable=False, @@ -80,6 +115,15 @@ class ReservationOccurrence(db.Model): nullable=True ) + link = db.relationship( + 'ReservationOccurrenceLink', + lazy=True, + backref=db.backref( + 'reservation_occurrence', + uselist=False + ) + ) + # relationship backrefs: # - reservation (Reservation.occurrences) @@ -205,6 +249,15 @@ def find_overlapping_with(cls, room, occurrences, skip_reservation_id=None): .options(contains_eager(ReservationOccurrence.reservation)) .options(cls.NO_RESERVATION_USER_STRATEGY)) + @property + def linked_object(self): + return self.link.object if self.link else None + + @linked_object.setter + def linked_object(self, obj): + assert self.link is None + self.link = ReservationOccurrenceLink(object=obj) + def can_reject(self, user, allow_admin=True): if not self.is_valid: return False diff --git a/indico/modules/rb/models/reservations.py b/indico/modules/rb/models/reservations.py index d70a907e6ef..900b6a7b4a8 100644 --- a/indico/modules/rb/models/reservations.py +++ b/indico/modules/rb/models/reservations.py @@ -21,8 +21,6 @@ from indico.core.db import db from indico.core.db.sqlalchemy.custom import PyIntEnum from indico.core.db.sqlalchemy.custom.utcdatetime import UTCDateTime -from indico.core.db.sqlalchemy.links import LinkMixin, LinkType -from indico.core.db.sqlalchemy.util.models import auto_table_args from indico.core.db.sqlalchemy.util.queries import limit_groups from indico.core.errors import NoReportError from indico.modules.rb.models.reservation_edit_logs import ReservationEditLog @@ -94,32 +92,6 @@ class ReservationState(IndicoIntEnum): rejected = 4 -class ReservationLink(LinkMixin, db.Model): - __tablename__ = 'reservation_links' - - @declared_attr - def __table_args__(cls): - return auto_table_args(cls, schema='roombooking') - - allowed_link_types = {LinkType.event, LinkType.contribution, LinkType.session_block} - events_backref_name = 'all_room_reservation_links' - link_backref_name = 'room_reservation_links' - - id = db.Column( - db.Integer, - primary_key=True - ) - - def __repr__(self): - return format_repr(self, 'id', _rawtext=self.link_repr) - - # relationship backrefs: - # - reservation (Reservation.link) - - -ReservationLink.register_link_events() - - class Reservation(db.Model): __tablename__ = 'reservations' @@ -208,12 +180,6 @@ def __table_args__(cls): db.String, nullable=True ) - link_id = db.Column( - db.Integer, - db.ForeignKey('roombooking.reservation_links.id'), - nullable=True, - index=True - ) end_notification_sent = db.Column( db.Boolean, nullable=False, @@ -259,15 +225,6 @@ def __table_args__(cls): ) ) - link = db.relationship( - 'ReservationLink', - lazy=True, - backref=db.backref( - 'reservation', - uselist=False - ) - ) - # relationship backrefs: # - room (Room.reservations) @@ -311,15 +268,6 @@ def location_name(self): def repetition(self): return self.repeat_frequency, self.repeat_interval, self.recurrence_weekdays - @property - def linked_object(self): - return self.link.object if self.link else None - - @linked_object.setter - def linked_object(self, obj): - assert self.link is None - self.link = ReservationLink(object=obj) - @property def event(self): return self.link.event if self.link else None diff --git a/indico/modules/rb/operations/bookings.py b/indico/modules/rb/operations/bookings.py index 582a803feaa..4d9582d56f3 100644 --- a/indico/modules/rb/operations/bookings.py +++ b/indico/modules/rb/operations/bookings.py @@ -24,7 +24,8 @@ from indico.modules.rb import rb_settings from indico.modules.rb.models.reservation_edit_logs import ReservationEditLog from indico.modules.rb.models.reservation_occurrences import ReservationOccurrence -from indico.modules.rb.models.reservations import RepeatFrequency, Reservation, ReservationLink +from indico.modules.rb.models.reservations import RepeatFrequency, Reservation +from indico.modules.rb.models.reservation_occurrences import ReservationOccurrenceLink from indico.modules.rb.models.room_nonbookable_periods import NonBookablePeriod from indico.modules.rb.models.rooms import Room from indico.modules.rb.operations.blockings import filter_blocked_rooms, get_rooms_blockings, group_blocked_rooms @@ -330,11 +331,12 @@ def create_booking_for_event(room_id, event): start_dt = event.start_dt.astimezone(default_timezone).replace(tzinfo=None) end_dt = event.end_dt.astimezone(default_timezone).replace(tzinfo=None) booking_reason = f"Event '{event.title}'" - data = {'start_dt': start_dt, 'end_dt': end_dt, 'booked_for_user': event.creator, - 'booking_reason': booking_reason, 'repeat_frequency': RepeatFrequency.NEVER, 'event_id': event.id} - booking = Reservation.create_from_data(room, data, session.user, ignore_admin=True) - booking.linked_object = event - return booking + data = {'event_id': event.id, 'start_dt': start_dt, 'end_dt': end_dt, 'booked_for_user': event.creator, + 'booking_reason': booking_reason, 'repeat_frequency': RepeatFrequency.DAY, 'repeat_interval': 1} + reservation = Reservation.create_from_data(room, data, session.user, ignore_admin=True) + for occurrence in reservation.occurrences: + occurrence.linked_object = event + return reservation except NoReportError: flash(_('Booking could not be created. Probably somebody else booked the room in the meantime.'), 'error') return None @@ -459,7 +461,9 @@ def get_matching_events(start_dt, end_dt, repeat_frequency, repeat_interval, rec excluded_categories = rb_settings.get('excluded_categories') return (Event.query .filter(~Event.is_deleted, - ~Event.room_reservation_links.any(ReservationLink.reservation.has(Reservation.is_accepted)), + # TODO: Test! This will probably blow up. + ~Event.room_reservation_occurrence_links.any( + ReservationOccurrenceLink.reservation.has(Reservation.is_accepted)), db.or_(Event.happens_between(server_to_utc(occ.start_dt), server_to_utc(occ.end_dt)) for occ in occurrences), Event.timezone == config.DEFAULT_TIMEZONE, diff --git a/indico/modules/rb/schemas.py b/indico/modules/rb/schemas.py index f8e6e67fb29..f151cf8ddd9 100644 --- a/indico/modules/rb/schemas.py +++ b/indico/modules/rb/schemas.py @@ -28,7 +28,7 @@ from indico.modules.rb.models.principals import RoomPrincipal from indico.modules.rb.models.reservation_edit_logs import ReservationEditLog from indico.modules.rb.models.reservation_occurrences import ReservationOccurrence -from indico.modules.rb.models.reservations import RepeatFrequency, Reservation, ReservationLink, ReservationState +from indico.modules.rb.models.reservations import RepeatFrequency, Reservation, ReservationState from indico.modules.rb.models.room_attributes import RoomAttribute, RoomAttributeAssociation from indico.modules.rb.models.room_bookable_hours import BookableHours from indico.modules.rb.models.room_features import RoomFeature @@ -216,15 +216,6 @@ def sort_logs(self, data, many, **kwargs): return data -class ReservationLinkSchema(mm.SQLAlchemyAutoSchema): - type = EnumField(LinkType, attribute='link_type') - id = Function(lambda link: link.object.id) - - class Meta: - model = ReservationLink - fields = ('type', 'id') - - class ReservationDetailsSchema(mm.SQLAlchemyAutoSchema): booked_for_user = Nested(UserSchema, only=('id', 'identifier', 'full_name', 'phone', 'email')) created_by_user = Nested(UserSchema, only=('id', 'identifier', 'full_name', 'email')) @@ -236,8 +227,6 @@ class ReservationDetailsSchema(mm.SQLAlchemyAutoSchema): can_reject = Function(lambda booking: booking.can_reject(session.user)) permissions = Method('_get_permissions') state = EnumField(ReservationState) - is_linked_to_object = Function(lambda booking: booking.link is not None) - link = Nested(ReservationLinkSchema) start_dt = NaiveDateTime() end_dt = NaiveDateTime() @@ -246,8 +235,7 @@ class Meta: fields = ('id', 'start_dt', 'end_dt', 'repetition', 'booking_reason', 'created_dt', 'booked_for_user', 'room_id', 'created_by_user', 'edit_logs', 'permissions', 'is_cancelled', 'is_rejected', 'is_accepted', 'is_pending', 'rejection_reason', - 'is_linked_to_object', 'link', 'state', 'external_details_url', 'internal_note', - 'recurrence_weekdays') + 'state', 'external_details_url', 'internal_note', 'recurrence_weekdays') def _get_permissions(self, booking): methods = ('can_accept', 'can_cancel', 'can_delete', 'can_edit', 'can_reject') diff --git a/indico/modules/rb/templates/booking_list.html b/indico/modules/rb/templates/booking_list.html index 284083ec1d8..5b40231071a 100644 --- a/indico/modules/rb/templates/booking_list.html +++ b/indico/modules/rb/templates/booking_list.html @@ -2,7 +2,7 @@ {% from 'forms/_form.html' import form_row %} {% block title %} - {% trans %}Event bookings{% endtrans %} + {% trans %}Room bookings{% endtrans %} {% endblock %} {% block content %} @@ -82,7 +82,7 @@ {% endif %} {% if links %}
-

Room bookings

+

Room booking occurrences

@@ -97,28 +97,26 @@

Room bookings

{% for link in links %} - {% set reservation = link.reservation %} + {% set occurrence = link.reservation_occurrence %} + {% set reservation = occurrence.reservation %} - -
{{ reservation.room.full_name }} - {% if reservation.is_rejected %} + {% if occurrence.is_rejected %} + title="{% trans %}Rejected:{% endtrans %} {{ occurrence.rejection_reason }}"> {% elif reservation.is_pending %} + title="{% trans %}Awaiting approval{% endtrans %}"> {% endif %} {{ reservation.booking_reason }} {{ reservation.booked_for_name }} - {{ reservation.start_dt | format_date()}} - {% if reservation.is_repeating %} - ({% trans %}recurring{% endtrans %}) - {% endif %} + + {{ occurrence.start_dt | format_date()}} - {{ reservation.start_dt | format_time() }} - {{ reservation.end_dt | format_time() }} + + {{ occurrence.start_dt | format_time() }} - {{ occurrence.end_dt | format_time() }}