De-avatarify most of room booking (except room owner)
[cds-indico.git] / indico / modules / rb / models / reservation_occurrences.py
blobf9cf076823dee0b273551da7a601833faeb04daf
1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 from datetime import datetime, date
18 from math import ceil
20 from dateutil import rrule
21 from sqlalchemy import Date, or_, func
22 from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
23 from sqlalchemy.sql import cast
25 from indico.core.db import db
26 from indico.core.db.sqlalchemy.util.queries import db_dates_overlap
27 from indico.core.errors import IndicoError
28 from indico.modules.rb.models.reservation_edit_logs import ReservationEditLog
29 from indico.modules.rb.models.utils import proxy_to_reservation_if_last_valid_occurrence
30 from indico.util import date_time
31 from indico.util.date_time import iterdays, format_date
32 from indico.util.serializer import Serializer
33 from indico.util.string import return_ascii
34 from indico.util.user import unify_user_args
37 class ReservationOccurrence(db.Model, Serializer):
38 __tablename__ = 'reservation_occurrences'
39 __table_args__ = {'schema': 'roombooking'}
40 __api_public__ = (('start_dt', 'startDT'), ('end_dt', 'endDT'), 'is_cancelled', 'is_rejected')
42 reservation_id = db.Column(
43 db.Integer,
44 db.ForeignKey('roombooking.reservations.id'),
45 nullable=False,
46 primary_key=True
48 start_dt = db.Column(
49 db.DateTime,
50 nullable=False,
51 primary_key=True,
52 index=True
54 end_dt = db.Column(
55 db.DateTime,
56 nullable=False,
57 index=True
59 notification_sent = db.Column(
60 db.Boolean,
61 nullable=False,
62 default=False
64 is_cancelled = db.Column(
65 db.Boolean,
66 nullable=False,
67 default=False
69 is_rejected = db.Column(
70 db.Boolean,
71 nullable=False,
72 default=False
74 rejection_reason = db.Column(
75 db.String
78 @hybrid_property
79 def date(self):
80 return self.start_dt.date()
82 @date.expression
83 def date(self):
84 return cast(self.start_dt, Date)
86 @hybrid_property
87 def is_valid(self):
88 return not self.is_rejected and not self.is_cancelled
90 @is_valid.expression
91 def is_valid(self):
92 return ~self.is_rejected & ~self.is_cancelled
94 @return_ascii
95 def __repr__(self):
96 return u'<ReservationOccurrence({0}, {1}, {2}, {3}, {4}, {5})>'.format(
97 self.reservation_id,
98 self.start_dt,
99 self.end_dt,
100 self.is_cancelled,
101 self.is_rejected,
102 self.notification_sent
105 @classmethod
106 def create_series_for_reservation(cls, reservation):
107 for o in cls.iter_create_occurrences(reservation.start_dt, reservation.end_dt, reservation.repetition):
108 o.reservation = reservation
110 @classmethod
111 def create_series(cls, start, end, repetition):
112 return list(cls.iter_create_occurrences(start, end, repetition))
114 @classmethod
115 def iter_create_occurrences(cls, start, end, repetition):
116 for start in cls.iter_start_time(start, end, repetition):
117 end = datetime.combine(start.date(), end.time())
118 yield ReservationOccurrence(start_dt=start, end_dt=end)
120 @staticmethod
121 def iter_start_time(start, end, repetition):
122 from indico.modules.rb.models.reservations import RepeatFrequency
124 repeat_frequency, repeat_interval = repetition
126 if repeat_frequency == RepeatFrequency.NEVER:
127 return [start]
129 if repeat_frequency == RepeatFrequency.DAY:
130 if repeat_interval == 1:
131 return rrule.rrule(rrule.DAILY, dtstart=start, until=end)
132 else:
133 raise IndicoError('Unsupported interval')
135 elif repeat_frequency == RepeatFrequency.WEEK:
136 if 0 < repeat_interval < 4:
137 return rrule.rrule(rrule.WEEKLY, dtstart=start, until=end, interval=repeat_interval)
138 else:
139 raise IndicoError('Unsupported interval')
141 elif repeat_frequency == RepeatFrequency.MONTH:
143 if repeat_interval == 1:
144 position = int(ceil(start.day / 7.0))
145 if position == 5:
146 # The fifth weekday of the month will always be the last one
147 position = -1
148 return rrule.rrule(rrule.MONTHLY, dtstart=start, until=end, byweekday=start.weekday(),
149 bysetpos=position)
150 else:
151 raise IndicoError('Unsupported interval {}'.format(repeat_interval))
153 raise IndicoError('Unexpected frequency {}'.format(repeat_frequency))
155 @staticmethod
156 def filter_overlap(occurrences):
157 return or_(db_dates_overlap(ReservationOccurrence, 'start_dt', occ.start_dt, 'end_dt', occ.end_dt)
158 for occ in occurrences)
160 @staticmethod
161 def find_overlapping_with(room, occurrences, skip_reservation_id=None):
162 from indico.modules.rb.models.reservations import Reservation
164 return ReservationOccurrence.find(Reservation.room == room,
165 Reservation.id != skip_reservation_id,
166 ReservationOccurrence.is_valid,
167 ReservationOccurrence.filter_overlap(occurrences),
168 _eager=ReservationOccurrence.reservation,
169 _join=Reservation)
171 @staticmethod
172 def find_with_filters(filters, user=None):
173 from indico.modules.rb.models.rooms import Room
174 from indico.modules.rb.models.reservations import Reservation
176 q = ReservationOccurrence.find(Room.is_active, _join=[Reservation, Room],
177 _eager=ReservationOccurrence.reservation)
179 if 'start_dt' in filters and 'end_dt' in filters:
180 start_dt = filters['start_dt']
181 end_dt = filters['end_dt']
182 criteria = []
183 # We have to check the time range for EACH DAY
184 for day_start_dt in iterdays(start_dt, end_dt):
185 # Same date, but the end time
186 day_end_dt = datetime.combine(day_start_dt.date(), end_dt.time())
187 criteria.append(db_dates_overlap(ReservationOccurrence, 'start_dt', day_start_dt, 'end_dt', day_end_dt))
188 q = q.filter(or_(*criteria))
190 if filters.get('is_only_mine') and user:
191 q = q.filter((Reservation.booked_for_id == user.id) | (Reservation.created_by_id == user.id))
192 if filters.get('room_ids'):
193 q = q.filter(Room.id.in_(filters['room_ids']))
195 if filters.get('is_only_confirmed_bookings') and not filters.get('is_only_pending_bookings'):
196 q = q.filter(Reservation.is_accepted)
197 elif not filters.get('is_only_confirmed_bookings') and filters.get('is_only_pending_bookings'):
198 q = q.filter(~Reservation.is_accepted)
200 if filters.get('is_rejected') and filters.get('is_cancelled'):
201 q = q.filter(Reservation.is_rejected | ReservationOccurrence.is_rejected
202 | Reservation.is_cancelled | ReservationOccurrence.is_cancelled)
203 else:
204 if filters.get('is_rejected'):
205 q = q.filter(Reservation.is_rejected | ReservationOccurrence.is_rejected)
206 else:
207 q = q.filter(~Reservation.is_rejected & ~ReservationOccurrence.is_rejected)
208 if filters.get('is_cancelled'):
209 q = q.filter(Reservation.is_cancelled | ReservationOccurrence.is_cancelled)
210 else:
211 q = q.filter(~Reservation.is_cancelled & ~ReservationOccurrence.is_cancelled)
213 if filters.get('is_archived'):
214 q = q.filter(Reservation.is_archived)
216 if filters.get('uses_vc'):
217 q = q.filter(Reservation.uses_vc)
218 if filters.get('needs_vc_assistance'):
219 q = q.filter(Reservation.needs_vc_assistance)
220 if filters.get('needs_assistance'):
221 q = q.filter(Reservation.needs_assistance)
223 if filters.get('booked_for_name'):
224 qs = u'%{}%'.format(filters['booked_for_name'])
225 q = q.filter(Reservation.booked_for_name.ilike(qs))
226 if filters.get('reason'):
227 qs = u'%{}%'.format(filters['reason'])
228 q = q.filter(Reservation.booking_reason.ilike(qs))
230 return q.order_by(Room.id)
232 @proxy_to_reservation_if_last_valid_occurrence
233 @unify_user_args
234 def cancel(self, user, reason=None, silent=False):
235 self.is_cancelled = True
236 self.rejection_reason = reason
237 if not silent:
238 log = [u'Day cancelled: {}'.format(format_date(self.date).decode('utf-8'))]
239 if reason:
240 log.append(u'Reason: {}'.format(reason))
241 self.reservation.add_edit_log(ReservationEditLog(user_name=user.full_name, info=log))
242 from indico.modules.rb.notifications.reservation_occurrences import notify_cancellation
243 notify_cancellation(self)
245 @proxy_to_reservation_if_last_valid_occurrence
246 @unify_user_args
247 def reject(self, user, reason, silent=False):
248 self.is_rejected = True
249 self.rejection_reason = reason
250 if not silent:
251 log = [u'Day rejected: {}'.format(format_date(self.date).decode('utf-8')),
252 u'Reason: {}'.format(reason)]
253 self.reservation.add_edit_log(ReservationEditLog(user_name=user.full_name, info=log))
254 from indico.modules.rb.notifications.reservation_occurrences import notify_rejection
255 notify_rejection(self)
257 def get_overlap(self, occurrence, skip_self=False):
258 if self.reservation and occurrence.reservation and self.reservation.room_id != occurrence.reservation.room_id:
259 raise ValueError('ReservationOccurrence objects of different rooms')
260 if skip_self and self.reservation and occurrence.reservation and self.reservation == occurrence.reservation:
261 return None, None
262 return date_time.get_overlap((self.start_dt, self.end_dt), (occurrence.start_dt, occurrence.end_dt))
264 def overlaps(self, occurrence, skip_self=False):
265 if self.reservation and occurrence.reservation and self.reservation.room_id != occurrence.reservation.room_id:
266 raise ValueError('ReservationOccurrence objects of different rooms')
267 if skip_self and self.reservation and occurrence.reservation and self.reservation == occurrence.reservation:
268 return False
269 return date_time.overlaps((self.start_dt, self.end_dt), (occurrence.start_dt, occurrence.end_dt))
271 @hybrid_method
272 def is_in_notification_window(self, exclude_first_day=False):
273 from indico.modules.rb import settings as rb_settings
274 if self.start_dt.date() < date.today():
275 return False
276 days_until_occurrence = (self.start_dt.date() - date.today()).days
277 notification_window = (self.reservation.room.notification_before_days
278 or rb_settings.get('notification_before_days', 1))
279 if exclude_first_day:
280 return days_until_occurrence < notification_window
281 else:
282 return days_until_occurrence <= notification_window
284 @is_in_notification_window.expression
285 def is_in_notification_window(self, exclude_first_day=False):
286 from indico.modules.rb import settings as rb_settings
287 from indico.modules.rb.models.rooms import Room
288 in_the_past = cast(self.start_dt, Date) < cast(func.now(), Date)
289 days_until_occurrence = cast(self.start_dt, Date) - cast(func.now(), Date)
290 notification_window = func.coalesce(Room.notification_before_days,
291 rb_settings.get('notification_before_days', 1))
292 if exclude_first_day:
293 return (days_until_occurrence < notification_window) & ~in_the_past
294 else:
295 return (days_until_occurrence <= notification_window) & ~in_the_past