diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7fa0ecbc3e0..12938dbd773 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -11,6 +11,8 @@ For the moment, we are focusing on _PostgreSQL_. These are some of the specific * ReservationEditLog uses a PostgreSQL ARRAY to store the changes for a single log entry +* calculate_rooms_booked_time uses `extract('dow')` PostgreSQL specific for day of the week. + In some cases you have properties in your models which trigger additional queries or are expensive for some other reason. Sometimes you can simply write tricky queries to retrieve all data at once, but in other cases that's not feasible, either because of what the property does or because you need it for serializing and thus only have the object itself diff --git a/indico/MaKaC/webinterface/tpls/RoomBookingAdminLocation.tpl b/indico/MaKaC/webinterface/tpls/RoomBookingAdminLocation.tpl index e2f106ac1f7..f24c9ec405c 100644 --- a/indico/MaKaC/webinterface/tpls/RoomBookingAdminLocation.tpl +++ b/indico/MaKaC/webinterface/tpls/RoomBookingAdminLocation.tpl @@ -373,7 +373,7 @@ indicoRequest( ${ _('Average occupancy') }: - ${ kpi['occupancy'] } + ${ '{0:.02f}'.format(kpi['occupancy'] * 100) }% ${inlineContextHelp('Average room occupancy in last 30 days during working hours (8H30-17H30, Monday-Friday including holidays). Only active, publically reservable rooms are taken into account.' )} diff --git a/indico/MaKaC/webinterface/tpls/RoomBookingRoomStats.tpl b/indico/MaKaC/webinterface/tpls/RoomBookingRoomStats.tpl index 31511b59ba6..754bc690311 100644 --- a/indico/MaKaC/webinterface/tpls/RoomBookingRoomStats.tpl +++ b/indico/MaKaC/webinterface/tpls/RoomBookingRoomStats.tpl @@ -143,7 +143,7 @@ % endif - ${ '{0:.02f}'.format(occupancy) }% + ${ '{0:.02f}'.format(occupancy * 100) }% diff --git a/indico/modules/rb/controllers/admin/locations.py b/indico/modules/rb/controllers/admin/locations.py index 6fa0ba093ac..f62ec036691 100644 --- a/indico/modules/rb/controllers/admin/locations.py +++ b/indico/modules/rb/controllers/admin/locations.py @@ -85,7 +85,7 @@ def _process(self): rooms = sorted(self._location.rooms, key=lambda r: natural_sort_key(r.full_name)) kpi = {} if self._with_kpi: - kpi['occupancy'] = calculate_rooms_occupancy(self._location.rooms) + kpi['occupancy'] = calculate_rooms_occupancy(self._location.rooms.all()) kpi['total_rooms'] = self._location.rooms.count() kpi['active_rooms'] = self._location.rooms.filter_by(is_active=True).count() kpi['reservable_rooms'] = self._location.rooms.filter_by(is_reservable=True).count() diff --git a/indico/modules/rb/controllers/user/rooms.py b/indico/modules/rb/controllers/user/rooms.py index 0190892c345..f457de10d0f 100644 --- a/indico/modules/rb/controllers/user/rooms.py +++ b/indico/modules/rb/controllers/user/rooms.py @@ -21,6 +21,7 @@ from dateutil.relativedelta import relativedelta from flask import request, session +from sqlalchemy import func from werkzeug.datastructures import MultiDict from MaKaC.common.cache import GenericCache @@ -174,7 +175,7 @@ def _checkParams(self): elif self._occupancy_period == 'thisyear': self._start = date(self._end.year, 1, 1) elif self._occupancy_period == 'sinceever': - self._start = Reservation.find().first().start_date.date() + self._start = Reservation.query.with_entities(func.min(Reservation.start_date)).one()[0].date() else: raise IndicoError('Invalid period specified') diff --git a/indico/modules/rb/models/locations.py b/indico/modules/rb/models/locations.py index 76c97a8e964..a50866c64d8 100644 --- a/indico/modules/rb/models/locations.py +++ b/indico/modules/rb/models/locations.py @@ -21,6 +21,8 @@ Holder of rooms in a place and its map view related data """ +from datetime import time + from sqlalchemy import func, or_, and_ from sqlalchemy.orm import aliased from sqlalchemy.sql.expression import select @@ -38,6 +40,9 @@ class Location(db.Model): __tablename__ = 'locations' + working_time_start = time(8) + working_time_end = time(17, 30) + # columns id = db.Column( diff --git a/indico/modules/rb/models/rooms.py b/indico/modules/rb/models/rooms.py index 838254ff5cd..b8e0138a816 100644 --- a/indico/modules/rb/models/rooms.py +++ b/indico/modules/rb/models/rooms.py @@ -23,10 +23,9 @@ import ast import json -from datetime import date, datetime, timedelta +from datetime import date, timedelta -from dateutil.relativedelta import relativedelta -from sqlalchemy import and_, func, exists, extract, or_, type_coerce +from sqlalchemy import and_, func, exists, or_, type_coerce from sqlalchemy.dialects.postgresql.base import ARRAY as sa_array from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import joinedload diff --git a/indico/modules/rb/statistics.py b/indico/modules/rb/statistics.py index 491b14733ca..32588f4ca8f 100644 --- a/indico/modules/rb/statistics.py +++ b/indico/modules/rb/statistics.py @@ -3,8 +3,11 @@ from datetime import date, datetime, timedelta from dateutil.relativedelta import relativedelta -from sqlalchemy import func +from sqlalchemy import func, extract, cast, TIME +from indico.core.db.sqlalchemy.custom import greatest, least +from indico.util.date_time import days_between +from indico.modules.rb.models.locations import Location from indico.modules.rb.models.reservations import Reservation from indico.modules.rb.models.reservation_occurrences import ReservationOccurrence @@ -13,29 +16,31 @@ def calculate_rooms_bookable_time(rooms, start_date=None, end_date=None): if end_date is None: end_date = datetime.utcnow() if start_date is None: - start_date = end_date + relativedelta(months=-1) - - total_days = (end_date - start_date).days + 1 - bookable_time = 0 - for room in rooms: - bookable_time += (total_days - room.get_nonbookable_days(start_date, end_date)) * room.bookable_time_per_day - - return bookable_time + start_date = end_date - relativedelta(months=1) + working_time_start = datetime.combine(date.today(), Location.working_time_start) + working_time_end = datetime.combine(date.today(), Location.working_time_end) + working_time_per_day = (working_time_end - working_time_start).seconds + working_days = days_between(start_date, end_date, include_weekends=False, inclusive=True) + return working_days * working_time_per_day * len(rooms) def calculate_rooms_booked_time(rooms, start_date=None, end_date=None): if end_date is None: end_date = date.today() if start_date is None: - start_date = end_date + relativedelta(months=-1) - - reservations = Reservation.find(Reservation.room_id.in_(r.id for r in rooms)) - query = (reservations.join(ReservationOccurrence) - .with_entities(func.sum(ReservationOccurrence.end - ReservationOccurrence.start)) - .filter(ReservationOccurrence.start >= start_date, - ReservationOccurrence.end <= end_date, - ReservationOccurrence.is_valid)) - return (query.scalar() or timedelta()).total_seconds() + start_date = end_date - relativedelta(months=1) + # Reservations on working days + reservations = Reservation.find(Reservation.room_id.in_(r.id for r in rooms), + extract('dow', ReservationOccurrence.start) < 5, + ReservationOccurrence.start >= start_date, + ReservationOccurrence.end <= end_date, + ReservationOccurrence.is_valid, + _join=ReservationOccurrence) + # Take into account only working hours + earliest_time = greatest(cast(ReservationOccurrence.start, TIME), Location.working_time_start) + latest_time = least(cast(ReservationOccurrence.end, TIME), Location.working_time_end) + booked_time = reservations.with_entities(func.sum(latest_time - earliest_time)).scalar() + return (booked_time or timedelta()).total_seconds() def calculate_rooms_occupancy(rooms, start=None, end=None):