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
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(
44 db
.ForeignKey('roombooking.reservations.id'),
59 notification_sent
= db
.Column(
64 is_cancelled
= db
.Column(
69 is_rejected
= db
.Column(
74 rejection_reason
= db
.Column(
80 return self
.start_dt
.date()
84 return cast(self
.start_dt
, Date
)
88 return not self
.is_rejected
and not self
.is_cancelled
92 return ~self
.is_rejected
& ~self
.is_cancelled
96 return u
'<ReservationOccurrence({0}, {1}, {2}, {3}, {4}, {5})>'.format(
102 self
.notification_sent
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
111 def create_series(cls
, start
, end
, repetition
):
112 return list(cls
.iter_create_occurrences(start
, end
, repetition
))
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
)
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
:
129 if repeat_frequency
== RepeatFrequency
.DAY
:
130 if repeat_interval
== 1:
131 return rrule
.rrule(rrule
.DAILY
, dtstart
=start
, until
=end
)
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
)
139 raise IndicoError('Unsupported interval')
141 elif repeat_frequency
== RepeatFrequency
.MONTH
:
143 if repeat_interval
== 1:
144 position
= int(ceil(start
.day
/ 7.0))
146 # The fifth weekday of the month will always be the last one
148 return rrule
.rrule(rrule
.MONTHLY
, dtstart
=start
, until
=end
, byweekday
=start
.weekday(),
151 raise IndicoError('Unsupported interval {}'.format(repeat_interval
))
153 raise IndicoError('Unexpected frequency {}'.format(repeat_frequency
))
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
)
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
,
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']
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
)
204 if filters
.get('is_rejected'):
205 q
= q
.filter(Reservation
.is_rejected | ReservationOccurrence
.is_rejected
)
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
)
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
234 def cancel(self
, user
, reason
=None, silent
=False):
235 self
.is_cancelled
= True
236 self
.rejection_reason
= reason
238 log
= [u
'Day cancelled: {}'.format(format_date(self
.date
).decode('utf-8'))]
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
247 def reject(self
, user
, reason
, silent
=False):
248 self
.is_rejected
= True
249 self
.rejection_reason
= reason
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
:
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
:
269 return date_time
.overlaps((self
.start_dt
, self
.end_dt
), (occurrence
.start_dt
, occurrence
.end_dt
))
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():
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
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
295 return (days_until_occurrence
<= notification_window
) & ~in_the_past