class SpecialtyPizza(models.Model): toppings = postgres_fields.ArrayField(models.CharField(max_length=20), size=4) metadata = postgres_fields.HStoreField() price_range = postgres_fields.IntegerRangeField() sales = postgres_fields.BigIntegerRangeField() available_on = postgres_fields.DateTimeRangeField() season = postgres_fields.DateRangeField()
def test_model_field_formfield_datetime(self): model_field = pg_fields.DateTimeRangeField() form_field = model_field.formfield() self.assertIsInstance(form_field, pg_forms.DateTimeRangeField) self.assertEqual( form_field.range_kwargs, {"bounds": pg_fields.ranges.CANONICAL_RANGE_BOUNDS}, )
class PostgresFieldsModel(models.Model): arrayfield = fields.ArrayField(models.CharField()) hstorefield = fields.HStoreField() jsonfield = fields.JSONField() rangefield = fields.RangeField() integerrangefield = fields.IntegerRangeField() bigintegerrangefield = fields.BigIntegerRangeField() floatrangefield = fields.FloatRangeField() datetimerangefield = fields.DateTimeRangeField() daterangefield = fields.DateRangeField() def arrayfield_tests(self): sorted_array = self.arrayfield.sort() print(sorted_array) def dictfield_tests(self): print(self.hstorefield.keys()) print(self.hstorefield.values()) print(self.hstorefield.update({'foo': 'bar'})) print(self.jsonfield.keys()) print(self.jsonfield.values()) print(self.jsonfield.update({'foo': 'bar'})) def rangefield_tests(self): print(self.rangefield.lower) print(self.rangefield.upper) print(self.integerrangefield.lower) print(self.integerrangefield.upper) print(self.bigintegerrangefield.lower) print(self.bigintegerrangefield.upper) print(self.floatrangefield.lower) print(self.floatrangefield.upper) print(self.datetimerangefield.lower) print(self.datetimerangefield.upper) print(self.daterangefield.lower) print(self.daterangefield.upper)
class Reservation(ModifiableModel): CREATED = 'created' CANCELLED = 'cancelled' CONFIRMED = 'confirmed' DENIED = 'denied' REQUESTED = 'requested' STATE_CHOICES = ( (CREATED, _('created')), (CANCELLED, _('cancelled')), (CONFIRMED, _('confirmed')), (DENIED, _('denied')), (REQUESTED, _('requested')), ) resource = models.ForeignKey('Resource', verbose_name=_('Resource'), db_index=True, related_name='reservations', on_delete=models.PROTECT) begin = models.DateTimeField(verbose_name=_('Begin time')) end = models.DateTimeField(verbose_name=_('End time')) duration = pgfields.DateTimeRangeField( verbose_name=_('Length of reservation'), null=True, blank=True, db_index=True) comments = models.TextField(null=True, blank=True, verbose_name=_('Comments')) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), null=True, blank=True, db_index=True, on_delete=models.PROTECT) state = models.CharField(max_length=16, choices=STATE_CHOICES, verbose_name=_('State'), default=CREATED) approver = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Approver'), related_name='approved_reservations', null=True, blank=True, on_delete=models.SET_NULL) staff_event = models.BooleanField(verbose_name=_('Is staff event'), default=False) # access-related fields access_code = models.CharField(verbose_name=_('Access code'), max_length=32, null=True, blank=True) # EXTRA FIELDS START HERE event_subject = models.CharField(max_length=200, verbose_name=_('Event subject'), blank=True) event_description = models.TextField(verbose_name=_('Event description'), blank=True) number_of_participants = models.PositiveSmallIntegerField( verbose_name=_('Number of participants'), blank=True, null=True) participants = models.TextField(verbose_name=_('Participants'), blank=True) host_name = models.CharField(verbose_name=_('Host name'), max_length=100, blank=True) reservation_extra_questions = models.TextField( verbose_name=_('Reservation extra questions'), blank=True) # extra detail fields for manually confirmed reservations reserver_name = models.CharField(verbose_name=_('Reserver name'), max_length=100, blank=True) reserver_id = models.CharField( verbose_name=_('Reserver ID (business or person)'), max_length=30, blank=True) reserver_email_address = models.EmailField( verbose_name=_('Reserver email address'), blank=True) reserver_phone_number = models.CharField( verbose_name=_('Reserver phone number'), max_length=30, blank=True) reserver_address_street = models.CharField( verbose_name=_('Reserver address street'), max_length=100, blank=True) reserver_address_zip = models.CharField( verbose_name=_('Reserver address zip'), max_length=30, blank=True) reserver_address_city = models.CharField( verbose_name=_('Reserver address city'), max_length=100, blank=True) company = models.CharField(verbose_name=_('Company'), max_length=100, blank=True) billing_address_street = models.CharField( verbose_name=_('Billing address street'), max_length=100, blank=True) billing_address_zip = models.CharField( verbose_name=_('Billing address zip'), max_length=30, blank=True) billing_address_city = models.CharField( verbose_name=_('Billing address city'), max_length=100, blank=True) # If the reservation was imported from another system, you can store the original ID in the field below. origin_id = models.CharField(verbose_name=_('Original ID'), max_length=50, editable=False, null=True) objects = ReservationQuerySet.as_manager() class Meta: verbose_name = _("reservation") verbose_name_plural = _("reservations") ordering = ('id', ) def _save_dt(self, attr, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ save_dt(self, attr, dt, self.resource.unit.time_zone) def _get_dt(self, attr, tz): return get_dt(self, attr, tz) @property def begin_tz(self): return self.begin @begin_tz.setter def begin_tz(self, dt): self._save_dt('begin', dt) def get_begin_tz(self, tz): return self._get_dt("begin", tz) @property def end_tz(self): return self.end @end_tz.setter def end_tz(self, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ self._save_dt('end', dt) def get_end_tz(self, tz): return self._get_dt("end", tz) def is_active(self): return self.end >= timezone.now() and self.state not in ( Reservation.CANCELLED, Reservation.DENIED) def is_own(self, user): if not (user and user.is_authenticated): return False return user == self.user def need_manual_confirmation(self): return self.resource.need_manual_confirmation def are_extra_fields_visible(self, user): # the following logic is used also implemented in ReservationQuerySet # so if this is changed that probably needs to be changed as well if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_access_code(self, user): if self.is_own(user): return True return self.resource.can_view_access_codes(user) def set_state(self, new_state, user): # Make sure it is a known state assert new_state in (Reservation.REQUESTED, Reservation.CONFIRMED, Reservation.DENIED, Reservation.CANCELLED) old_state = self.state if new_state == old_state: if old_state == Reservation.CONFIRMED: reservation_modified.send(sender=self.__class__, instance=self, user=user) return if new_state == Reservation.CONFIRMED: self.approver = user reservation_confirmed.send(sender=self.__class__, instance=self, user=user) elif old_state == Reservation.CONFIRMED: self.approver = None user_is_staff = self.user is not None and self.user.is_staff # Notifications if new_state == Reservation.REQUESTED: self.send_reservation_requested_mail() self.send_reservation_requested_mail_to_officials() elif new_state == Reservation.CONFIRMED: if self.need_manual_confirmation(): self.send_reservation_confirmed_mail() elif self.access_code: self.send_reservation_created_with_access_code_mail() else: if not user_is_staff: # notifications are not sent from staff created reservations to avoid spam self.send_reservation_created_mail() elif new_state == Reservation.DENIED: self.send_reservation_denied_mail() elif new_state == Reservation.CANCELLED: if user != self.user: self.send_reservation_cancelled_mail() reservation_cancelled.send(sender=self.__class__, instance=self, user=user) self.state = new_state self.save() def can_modify(self, user): if not user: return False # reservations that need manual confirmation and are confirmed cannot be # modified or cancelled without reservation approve permission cannot_approve = not self.resource.can_approve_reservations(user) if self.need_manual_confirmation( ) and self.state == Reservation.CONFIRMED and cannot_approve: return False return self.user == user or self.resource.can_modify_reservations(user) def can_add_comment(self, user): if self.is_own(user): return True return self.resource.can_access_reservation_comments(user) def can_view_field(self, user, field): if field not in RESERVATION_EXTRA_FIELDS: return True if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_catering_orders(self, user): if self.is_own(user): return True return self.resource.can_view_catering_orders(user) def format_time(self): tz = self.resource.unit.get_tz() begin = self.begin.astimezone(tz) end = self.end.astimezone(tz) return format_dt_range(translation.get_language(), begin, end) def __str__(self): if self.state != Reservation.CONFIRMED: state_str = ' (%s)' % self.state else: state_str = '' return "%s: %s%s" % (self.format_time(), self.resource, state_str) def clean(self, **kwargs): """ Check restrictions that are common to all reservations. If this reservation isn't yet saved and it will modify an existing reservation, the original reservation need to be provided in kwargs as 'original_reservation', so that it can be excluded when checking if the resource is available. """ if self.end <= self.begin: raise ValidationError( _("You must end the reservation after it has begun")) # Check that begin and end times are on valid time slots. opening_hours = self.resource.get_opening_hours( self.begin.date(), self.end.date()) for dt in (self.begin, self.end): days = opening_hours.get(dt.date(), []) day = next((day for day in days if day['opens'] is not None and day['opens'] <= dt <= day['closes']), None) if day and not is_valid_time_slot(dt, self.resource.slot_size, day['opens']): raise ValidationError( _("Begin and end time must match time slots"), code='invalid_time_slot') original_reservation = self if self.pk else kwargs.get( 'original_reservation', None) if self.resource.check_reservation_collision(self.begin, self.end, original_reservation): raise ValidationError( _("The resource is already reserved for some of the period")) if (self.end - self.begin) < self.resource.min_period: raise ValidationError( _("The minimum reservation length is %(min_period)s") % {'min_period': humanize_duration(self.resource.min_period)}) if self.access_code: validate_access_code(self.access_code, self.resource.access_code_type) def get_notification_context(self, language_code, user=None, notification_type=None): if not user: user = self.user with translation.override(language_code): reserver_name = self.reserver_name reserver_email_address = self.reserver_email_address if not reserver_name and self.user and self.user.get_display_name( ): reserver_name = self.user.get_display_name() if not reserver_email_address and user and user.email: reserver_email_address = user.email context = { 'resource': self.resource.name, 'begin': localize_datetime(self.begin), 'end': localize_datetime(self.end), 'begin_dt': self.begin, 'end_dt': self.end, 'time_range': self.format_time(), 'number_of_participants': self.number_of_participants, 'host_name': self.host_name, 'reserver_name': reserver_name, 'event_subject': self.event_subject, 'event_description': self.event_description, 'reserver_email_address': reserver_email_address, 'reserver_phone_number': self.reserver_phone_number, } if self.resource.unit: context['unit'] = self.resource.unit.name context['unit_id'] = self.resource.unit.id if self.can_view_access_code(user) and self.access_code: context['access_code'] = self.access_code if notification_type == NotificationType.RESERVATION_CONFIRMED: if self.resource.reservation_confirmed_notification_extra: context[ 'extra_content'] = self.resource.reservation_confirmed_notification_extra elif notification_type == NotificationType.RESERVATION_REQUESTED: if self.resource.reservation_requested_notification_extra: context[ 'extra_content'] = self.resource.reservation_requested_notification_extra # Get last main and ground plan images. Normally there shouldn't be more than one of each # of those images. images = self.resource.images.filter( type__in=('main', 'ground_plan')).order_by('-sort_order') main_image = next((i for i in images if i.type == 'main'), None) ground_plan_image = next( (i for i in images if i.type == 'ground_plan'), None) if main_image: main_image_url = main_image.get_full_url() if main_image_url: context['resource_main_image_url'] = main_image_url if ground_plan_image: ground_plan_image_url = ground_plan_image.get_full_url() if ground_plan_image_url: context[ 'resource_ground_plan_image_url'] = ground_plan_image_url return context def send_reservation_mail(self, notification_type, user=None, attachments=None): """ Stuff common to all reservation related mails. If user isn't given use self.user. """ try: notification_template = NotificationTemplate.objects.get( type=notification_type) except NotificationTemplate.DoesNotExist: return if user: email_address = user.email else: if not (self.reserver_email_address or self.user): return email_address = self.reserver_email_address or self.user.email user = self.user language = user.get_preferred_language() if user else DEFAULT_LANG context = self.get_notification_context( language, notification_type=notification_type) try: rendered_notification = notification_template.render( context, language) except NotificationTemplateException as e: logger.error(e, exc_info=True, extra={'user': user.uuid}) return send_respa_mail(email_address, rendered_notification['subject'], rendered_notification['body'], rendered_notification['html_body'], attachments) def send_reservation_requested_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_REQUESTED) def send_reservation_requested_mail_to_officials(self): notify_users = self.resource.get_users_with_perm( 'can_approve_reservation') if len(notify_users) > 100: raise Exception("Refusing to notify more than 100 users (%s)" % self) for user in notify_users: self.send_reservation_mail( NotificationType.RESERVATION_REQUESTED_OFFICIAL, user=user) def send_reservation_denied_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_DENIED) def send_reservation_confirmed_mail(self): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = ('reservation.ics', ical_file, 'text/calendar') self.send_reservation_mail(NotificationType.RESERVATION_CONFIRMED, attachments=[attachment]) def send_reservation_cancelled_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_CANCELLED) def send_reservation_created_mail(self): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = 'reservation.ics', ical_file, 'text/calendar' self.send_reservation_mail(NotificationType.RESERVATION_CREATED, attachments=[attachment]) def send_reservation_created_with_access_code_mail(self): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = 'reservation.ics', ical_file, 'text/calendar' self.send_reservation_mail( NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE, attachments=[attachment]) def send_access_code_created_mail(self): self.send_reservation_mail( NotificationType.RESERVATION_ACCESS_CODE_CREATED) def save(self, *args, **kwargs): self.duration = DateTimeTZRange(self.begin, self.end, '[)') if not self.access_code: access_code_type = self.resource.access_code_type if self.resource.is_access_code_enabled( ) and self.resource.generate_access_codes: self.access_code = generate_access_code(access_code_type) return super().save(*args, **kwargs)
def test_model_field_formfield_datetime(self): model_field = pg_fields.DateTimeRangeField() form_field = model_field.formfield() self.assertIsInstance(form_field, pg_forms.DateTimeRangeField)
def test_model_field_formfield_datetime_default_bounds(self): model_field = pg_fields.DateTimeRangeField(default_bounds="[]") form_field = model_field.formfield() self.assertIsInstance(form_field, pg_forms.DateTimeRangeField) self.assertEqual(form_field.range_kwargs, {"bounds": "[]"})
class Reservation(ModifiableModel): CREATED = 'created' CANCELLED = 'cancelled' CONFIRMED = 'confirmed' DENIED = 'denied' REQUESTED = 'requested' WAITING_FOR_PAYMENT = 'waiting_for_payment' STATE_CHOICES = ( (CREATED, _('created')), (CANCELLED, _('cancelled')), (CONFIRMED, _('confirmed')), (DENIED, _('denied')), (REQUESTED, _('requested')), (WAITING_FOR_PAYMENT, _('waiting for payment')), ) TYPE_NORMAL = 'normal' TYPE_BLOCKED = 'blocked' TYPE_CHOICES = ( (TYPE_NORMAL, _('Normal reservation')), (TYPE_BLOCKED, _('Resource blocked')), ) resource = models.ForeignKey('Resource', verbose_name=_('Resource'), db_index=True, related_name='reservations', on_delete=models.PROTECT) begin = models.DateTimeField(verbose_name=_('Begin time')) end = models.DateTimeField(verbose_name=_('End time')) duration = pgfields.DateTimeRangeField( verbose_name=_('Length of reservation'), null=True, blank=True, db_index=True) comments = models.TextField(null=True, blank=True, verbose_name=_('Comments')) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), null=True, blank=True, db_index=True, on_delete=models.PROTECT) preferred_language = models.CharField(choices=settings.LANGUAGES, verbose_name='Preferred Language', null=True, default=settings.LANGUAGES[0][0], max_length=8) state = models.CharField(max_length=32, choices=STATE_CHOICES, verbose_name=_('State'), default=CREATED) approver = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Approver'), related_name='approved_reservations', null=True, blank=True, on_delete=models.SET_NULL) staff_event = models.BooleanField(verbose_name=_('Is staff event'), default=False) type = models.CharField(blank=False, verbose_name=_('Type'), max_length=32, choices=TYPE_CHOICES, default=TYPE_NORMAL) has_arrived = models.BooleanField(verbose_name=_('Has arrived'), default=False) # access-related fields access_code = models.CharField(verbose_name=_('Access code'), max_length=32, null=True, blank=True) # EXTRA FIELDS START HERE event_subject = models.CharField(max_length=200, verbose_name=_('Event subject'), blank=True) event_description = models.TextField(verbose_name=_('Event description'), blank=True) number_of_participants = models.PositiveSmallIntegerField( verbose_name=_('Number of participants'), blank=True, null=True, default=1) participants = models.TextField(verbose_name=_('Participants'), blank=True) host_name = models.CharField(verbose_name=_('Host name'), max_length=100, blank=True) require_assistance = models.BooleanField( verbose_name=_('Require assistance'), default=False) require_workstation = models.BooleanField( verbose_name=_('Require workstation'), default=False) home_municipality = models.ForeignKey('ReservationHomeMunicipalityField', verbose_name=_('Home municipality'), null=True, blank=True, on_delete=models.SET_NULL) # extra detail fields for manually confirmed reservations reserver_name = models.CharField(verbose_name=_('Reserver name'), max_length=100, blank=True) reserver_id = models.CharField( verbose_name=_('Reserver ID (business or person)'), max_length=30, blank=True) reserver_email_address = models.EmailField( verbose_name=_('Reserver email address'), blank=True) reserver_phone_number = models.CharField( verbose_name=_('Reserver phone number'), max_length=30, blank=True) reserver_address_street = models.CharField( verbose_name=_('Reserver address street'), max_length=100, blank=True) reserver_address_zip = models.CharField( verbose_name=_('Reserver address zip'), max_length=30, blank=True) reserver_address_city = models.CharField( verbose_name=_('Reserver address city'), max_length=100, blank=True) reservation_extra_questions = models.TextField( verbose_name=_('Reservation extra questions'), blank=True) company = models.CharField(verbose_name=_('Company'), max_length=100, blank=True) billing_first_name = models.CharField(verbose_name=_('Billing first name'), max_length=100, blank=True) billing_last_name = models.CharField(verbose_name=_('Billing last name'), max_length=100, blank=True) billing_email_address = models.EmailField( verbose_name=_('Billing email address'), blank=True) billing_phone_number = models.CharField( verbose_name=_('Billing phone number'), max_length=30, blank=True) billing_address_street = models.CharField( verbose_name=_('Billing address street'), max_length=100, blank=True) billing_address_zip = models.CharField( verbose_name=_('Billing address zip'), max_length=30, blank=True) billing_address_city = models.CharField( verbose_name=_('Billing address city'), max_length=100, blank=True) # If the reservation was imported from another system, you can store the original ID in the field below. origin_id = models.CharField(verbose_name=_('Original ID'), max_length=50, editable=False, null=True) reminder = models.ForeignKey('ReservationReminder', verbose_name=_('Reservation Reminder'), db_index=True, related_name='ReservationReminders', on_delete=models.SET_NULL, null=True, blank=True) objects = ReservationQuerySet.as_manager() class Meta: verbose_name = _("reservation") verbose_name_plural = _("reservations") ordering = ('id', ) def _save_dt(self, attr, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ save_dt(self, attr, dt, self.resource.unit.time_zone) def _get_dt(self, attr, tz): return get_dt(self, attr, tz) @property def begin_tz(self): return self.begin @begin_tz.setter def begin_tz(self, dt): self._save_dt('begin', dt) def get_begin_tz(self, tz): return self._get_dt("begin", tz) @property def end_tz(self): return self.end @end_tz.setter def end_tz(self, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ self._save_dt('end', dt) def get_end_tz(self, tz): return self._get_dt("end", tz) def is_active(self): print( self.end + self.resource.cooldown >= timezone.now() and self.state not in (Reservation.CANCELLED, Reservation.DENIED)) return self.end + self.resource.cooldown >= timezone.now( ) and self.state not in (Reservation.CANCELLED, Reservation.DENIED) def is_own(self, user): if not (user and user.is_authenticated): return False return user == self.user def need_manual_confirmation(self): return self.resource.need_manual_confirmation def are_extra_fields_visible(self, user): # the following logic is used also implemented in ReservationQuerySet # so if this is changed that probably needs to be changed as well if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_access_code(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_access_code(user) def set_state(self, new_state, user): # Make sure it is a known state assert new_state in (Reservation.REQUESTED, Reservation.CONFIRMED, Reservation.DENIED, Reservation.CANCELLED, Reservation.WAITING_FOR_PAYMENT) old_state = self.state if new_state == old_state: if old_state == Reservation.CONFIRMED: reservation_modified.send(sender=self.__class__, instance=self, user=user) return if new_state == Reservation.CONFIRMED: self.approver = user if user and user.is_authenticated else None if user and user.is_authenticated or self.resource.authentication == 'unauthenticated': reservation_confirmed.send(sender=self.__class__, instance=self, user=user) elif old_state == Reservation.CONFIRMED: self.approver = None user_is_staff = self.user is not None and self.user.is_staff # Notifications if new_state == Reservation.REQUESTED: if not user_is_staff: self.send_reservation_requested_mail() self.notify_staff_about_reservation( NotificationType.RESERVATION_REQUESTED_OFFICIAL) else: if self.reserver_email_address != self.user.email: self.send_reservation_requested_mail( action_by_official=True) elif new_state == Reservation.CONFIRMED: if self.need_manual_confirmation(): self.send_reservation_confirmed_mail() elif self.access_code: if not user_is_staff: self.send_reservation_created_with_access_code_mail() self.notify_staff_about_reservation( NotificationType. RESERVATION_CREATED_WITH_ACCESS_CODE_OFFICIAL) else: if self.reserver_email_address != self.user.email: self.send_reservation_created_with_access_code_mail( action_by_official=True) else: if not user_is_staff: self.send_reservation_created_mail() self.notify_staff_about_reservation( NotificationType.RESERVATION_CREATED_OFFICIAL) else: if self.reserver_email_address != self.user.email: self.send_reservation_created_mail( action_by_official=True) self.notify_staff_about_reservation( NotificationType.RESERVATION_CREATED_OFFICIAL) elif new_state == Reservation.DENIED: self.send_reservation_denied_mail() elif new_state == Reservation.CANCELLED: if self.user: order = self.get_order() if order: if order.state == order.CANCELLED: self.send_reservation_cancelled_mail() else: if user.is_staff and (user.email != self.user.email ): # Assume staff cancelled it self.send_reservation_cancelled_mail( action_by_official=True) else: self.send_reservation_cancelled_mail() self.notify_staff_about_reservation( NotificationType.RESERVATION_CANCELLED_OFFICIAL) reservation_cancelled.send(sender=self.__class__, instance=self, user=user) self.state = new_state self.save() def can_modify(self, user): if not user: return False if self.state == Reservation.WAITING_FOR_PAYMENT: return False if self.get_order(): return self.resource.can_modify_paid_reservations(user) # reservations that need manual confirmation and are confirmed cannot be # modified or cancelled without reservation approve permission cannot_approve = not self.resource.can_approve_reservations(user) if self.need_manual_confirmation( ) and self.state == Reservation.CONFIRMED and cannot_approve: return False return self.user == user or self.resource.can_modify_reservations(user) def can_add_comment(self, user): if self.is_own(user): return True return self.resource.can_access_reservation_comments(user) def can_view_field(self, user, field): if field not in RESERVATION_EXTRA_FIELDS: return True if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_catering_orders(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_catering_orders(user) def can_add_product_order(self, user): return self.is_own(user) def can_view_product_orders(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_product_orders(user) def get_order(self): return getattr(self, 'order', None) def format_time(self): tz = self.resource.unit.get_tz() begin = self.begin.astimezone(tz) end = self.end.astimezone(tz) return format_dt_range(translation.get_language(), begin, end) def create_reminder(self): r_date = self.begin - datetime.timedelta( hours=int(self.resource.unit.sms_reminder_delay)) reminder = ReservationReminder() reminder.reservation = self reminder.reminder_date = r_date reminder.save() self.reminder = reminder def modify_reminder(self): if not self.reminder: return r_date = self.begin - datetime.timedelta( hours=int(self.resource.unit.sms_reminder_delay)) self.reminder.reminder_date = r_date self.reminder.save() def __str__(self): if self.state != Reservation.CONFIRMED: state_str = ' (%s)' % self.state else: state_str = '' return "%s: %s%s" % (self.format_time(), self.resource, state_str) def clean(self, **kwargs): """ Check restrictions that are common to all reservations. If this reservation isn't yet saved and it will modify an existing reservation, the original reservation need to be provided in kwargs as 'original_reservation', so that it can be excluded when checking if the resource is available. """ if 'user' in kwargs: user = kwargs['user'] else: user = self.user user_is_admin = user and self.resource.is_admin(user) if self.end <= self.begin: raise ValidationError( _("You must end the reservation after it has begun")) # Check that begin and end times are on valid time slots. opening_hours = self.resource.get_opening_hours( self.begin.date(), self.end.date()) for dt in (self.begin, self.end): days = opening_hours.get(dt.date(), []) day = next((day for day in days if day['opens'] is not None and day['opens'] <= dt <= day['closes']), None) if day and not is_valid_time_slot(dt, self.resource.slot_size, day['opens']): raise ValidationError( _("Begin and end time must match time slots"), code='invalid_time_slot') # Check if Unit has disallow_overlapping_reservations value of True if (self.resource.unit.disallow_overlapping_reservations and not self.resource.can_create_overlapping_reservations(user)): reservations_for_same_unit = Reservation.objects.filter( user=user, resource__unit=self.resource.unit) valid_reservations_for_same_unit = reservations_for_same_unit.exclude( state=Reservation.CANCELLED) user_has_conflicting_reservations = valid_reservations_for_same_unit.filter( Q(begin__gt=self.begin, begin__lt=self.end) | Q(begin__lt=self.begin, end__gt=self.begin) | Q(begin__gte=self.begin, end__lte=self.end)) if user_has_conflicting_reservations: raise ValidationError(_( 'This unit does not allow overlapping reservations for its resources' ), code='conflicting_reservation') original_reservation = self if self.pk else kwargs.get( 'original_reservation', None) if self.resource.check_reservation_collision(self.begin, self.end, original_reservation): raise ValidationError( _("The resource is already reserved for some of the period")) if not user_is_admin: if (self.end - self.begin) < self.resource.min_period: raise ValidationError( _("The minimum reservation length is %(min_period)s") % { 'min_period': humanize_duration( self.resource.min_period) }) else: if not (self.end - self.begin ) % self.resource.slot_size == datetime.timedelta(0): raise ValidationError( _("The minimum reservation length is %(slot_size)s") % {'slot_size': humanize_duration(self.resource.slot_size)}) if self.access_code: validate_access_code(self.access_code, self.resource.access_code_type) if self.resource.people_capacity: if (self.number_of_participants > self.resource.people_capacity): raise ValidationError( _("This resource has people capacity limit of %s" % self.resource.people_capacity)) def get_notification_context(self, language_code, user=None, notification_type=None, extra_context={}): if not user: user = self.user with translation.override(language_code): reserver_name = self.reserver_name reserver_email_address = self.reserver_email_address if not reserver_name and self.user and self.user.get_display_name( ): reserver_name = self.user.get_display_name() if not reserver_email_address and user and user.email: reserver_email_address = user.email context = { 'resource': self.resource.name, 'begin': localize_datetime(self.begin), 'end': localize_datetime(self.end), 'begin_dt': self.begin, 'end_dt': self.end, 'time_range': self.format_time(), 'reserver_name': reserver_name, 'reserver_email_address': reserver_email_address, 'require_assistance': self.require_assistance, 'require_workstation': self.require_workstation, 'extra_question': self.reservation_extra_questions } directly_included_fields = ( 'number_of_participants', 'host_name', 'event_subject', 'event_description', 'reserver_phone_number', 'billing_first_name', 'billing_last_name', 'billing_email_address', 'billing_phone_number', 'billing_address_street', 'billing_address_zip', 'billing_address_city', ) for field in directly_included_fields: context[field] = getattr(self, field) if self.resource.unit: context['unit'] = self.resource.unit.name context[ 'unit_address'] = self.resource.unit.address_postal_full context['unit_id'] = self.resource.unit.id context[ 'unit_map_service_id'] = self.resource.unit.map_service_id if self.can_view_access_code(user) and self.access_code: context['access_code'] = self.access_code if self.user and self.user.is_staff: context['staff_name'] = self.user.get_display_name() if notification_type in [ NotificationType.RESERVATION_CONFIRMED, NotificationType.RESERVATION_CREATED ]: if self.resource.reservation_confirmed_notification_extra: context[ 'extra_content'] = self.resource.reservation_confirmed_notification_extra elif notification_type == NotificationType.RESERVATION_REQUESTED: if self.resource.reservation_requested_notification_extra: context[ 'extra_content'] = self.resource.reservation_requested_notification_extra # Get last main and ground plan images. Normally there shouldn't be more than one of each # of those images. images = self.resource.images.filter( type__in=('main', 'ground_plan')).order_by('-sort_order') main_image = next((i for i in images if i.type == 'main'), None) ground_plan_image = next( (i for i in images if i.type == 'ground_plan'), None) if main_image: main_image_url = main_image.get_full_url() if main_image_url: context['resource_main_image_url'] = main_image_url if ground_plan_image: ground_plan_image_url = ground_plan_image.get_full_url() if ground_plan_image_url: context[ 'resource_ground_plan_image_url'] = ground_plan_image_url order = getattr(self, 'order', None) if order: context['order'] = order.get_notification_context( language_code) if extra_context: context.update({'bulk_email_context': {**extra_context}}) return context def send_reservation_mail(self, notification_type, user=None, attachments=None, action_by_official=False, staff_email=None, extra_context={}, is_reminder=False): if self.resource.unit.sms_reminder: # only allow certain notification types as reminders e.g. exclude reservation_access_code_created allowed_reminder_notification_types = ( NotificationType.RESERVATION_CONFIRMED, NotificationType.RESERVATION_CREATED, NotificationType.RESERVATION_CREATED_BY_OFFICIAL, NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE, NotificationType. RESERVATION_CREATED_WITH_ACCESS_CODE_BY_OFFICIAL, ) if self.reminder and notification_type in allowed_reminder_notification_types: self.reminder.notification_type = self.reminder.notification_type if self.reminder.notification_type else notification_type self.reminder.user = self.reminder.user if self.reminder.user else user self.reminder.action_by_official = self.reminder.action_by_official if self.reminder.action_by_official else action_by_official self.reminder.save() """ Stuff common to all reservation related mails. If user isn't given use self.user. """ try: notification_template = NotificationTemplate.objects.get( type=notification_type) except NotificationTemplate.DoesNotExist: print('Notification type: %s does not exist' % notification_type) return if getattr(self, 'order', None) and self.billing_email_address: email_address = self.billing_email_address elif user: email_address = user.email else: if not (self.reserver_email_address or self.user): return if action_by_official: email_address = self.reserver_email_address else: email_address = self.reserver_email_address or self.user.email user = self.user language = DEFAULT_LANG if user and not user.is_staff: language = self.preferred_language context = self.get_notification_context( language, notification_type=notification_type, extra_context=extra_context) try: if staff_email: language = DEFAULT_LANG rendered_notification = notification_template.render( context, language) except NotificationTemplateException as e: print('NotifcationTemplateException: %s' % e) logger.error(e, exc_info=True, extra={'user': user.uuid}) return if is_reminder: print("Sending SMS notification :: (%s) %s || LOCALE: %s" % (self.reserver_phone_number, rendered_notification['subject'], language)) ret = send_respa_mail( email_address='%s@%s' % (self.reserver_phone_number, settings.GSM_NOTIFICATION_ADDRESS), subject=rendered_notification['subject'], body=rendered_notification['short_message'], ) print(ret[1]) return if staff_email: print("Sending automated mail :: (%s) %s || LOCALE: %s" % (staff_email, rendered_notification['subject'], language)) ret = send_respa_mail(staff_email, rendered_notification['subject'], rendered_notification['body'], rendered_notification['html_body'], attachments) print(ret[1]) else: print("Sending automated mail :: (%s) %s || LOCALE: %s" % (email_address, rendered_notification['subject'], language)) ret = send_respa_mail(email_address, rendered_notification['subject'], rendered_notification['body'], rendered_notification['html_body'], attachments) print(ret[1]) def notify_staff_about_reservation(self, notification): if self.resource.resource_staff_emails: for email in self.resource.resource_staff_emails: self.send_reservation_mail(notification, staff_email=email) else: notify_users = self.resource.get_users_with_perm( 'can_approve_reservation') if len(notify_users) > 100: raise Exception("Refusing to notify more than 100 users (%s)" % self) for user in notify_users: self.send_reservation_mail(notification, user=user) def send_reservation_requested_mail(self, action_by_official=False): notification = NotificationType.RESERVATION_REQUESTED_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_REQUESTED self.send_reservation_mail(notification) def send_reservation_modified_mail(self, action_by_official=False): notification = NotificationType.RESERVATION_MODIFIED_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_MODIFIED self.send_reservation_mail(notification, action_by_official=action_by_official) def send_reservation_denied_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_DENIED) def send_reservation_confirmed_mail(self): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = ('reservation.ics', ical_file, 'text/calendar') self.send_reservation_mail(NotificationType.RESERVATION_CONFIRMED, attachments=[attachment]) def send_reservation_cancelled_mail(self, action_by_official=False): notification = NotificationType.RESERVATION_CANCELLED_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_CANCELLED self.send_reservation_mail(notification, action_by_official=action_by_official) def send_reservation_created_mail(self, action_by_official=False): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = 'reservation.ics', ical_file, 'text/calendar' notification = NotificationType.RESERVATION_CREATED_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_CREATED self.send_reservation_mail(notification, attachments=[attachment], action_by_official=action_by_official) def send_reservation_created_with_access_code_mail(self, action_by_official=False ): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = 'reservation.ics', ical_file, 'text/calendar' notification = NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE_OFFICIAL_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE self.send_reservation_mail(notification, attachments=[attachment], action_by_official=action_by_official) def send_access_code_created_mail(self): self.send_reservation_mail( NotificationType.RESERVATION_ACCESS_CODE_CREATED) def save(self, *args, **kwargs): self.duration = DateTimeTZRange(self.begin, self.end, '[)') if not self.access_code: access_code_type = self.resource.access_code_type if self.resource.is_access_code_enabled( ) and self.resource.generate_access_codes: self.access_code = generate_access_code(access_code_type) return super().save(*args, **kwargs)
class Cup(models.Model): INTERVAL_CHOICES = ((0, _('Daily')), (1, _('Weekly')), (2, _('Monthly'))) class Meta: verbose_name = _('Coffee Cup') app_label = "coffeemaker" ordering = ['-date_created'] created_by = models.ForeignKey(User, verbose_name=_('Author'), related_name='cup_authors') owl = models.ForeignKey(User, verbose_name=_('Owl'), related_name='cup_owls', blank=True, null=True) table = models.ForeignKey(Table, verbose_name=_('Table'), blank=True, null=True) followers = models.ManyToManyField(User, blank=True, null=True) title = models.CharField(_('Title'), max_length=255, blank=True) description = models.TextField(_('Description'), blank=True) completed = models.BooleanField(_('Completed'), default=False) public = models.BooleanField(_('Public'), default=False) locked = models.BooleanField(_('Locked'), default=False) # Repetitive tasks set_to_repeat = models.BooleanField(_('Set to repeat'), default=False) repeat_interval = models.PositiveSmallIntegerField( verbose_name=_('Repeat interval'), choices=INTERVAL_CHOICES, blank=True, null=True) repeat_months = pg_fields.ArrayField( verbose_name=_('Months to be repeated'), base_field=models.PositiveSmallIntegerField(choices=MONTHS.items()), size=12, blank=True, null=True) repeat_days = pg_fields.ArrayField( verbose_name=_('Days to be repeated'), base_field=models.PositiveSmallIntegerField( choices=WEEKDAYS_ABBR.items()), size=12, blank=True, null=True) repeat_date_range = pg_fields.DateTimeRangeField( verbose_name=_('Repeat date range'), blank=True, null=True) # Time information calendar_date_range = pg_fields.DateTimeRangeField(_('Time range'), blank=True, null=True) due_date = models.DateTimeField(_('Due date'), blank=True, null=True) date_created = models.DateTimeField(_('Date Created'), auto_now_add=True) last_modified = models.DateTimeField(_('Last Modified'), auto_now=True) def __str__(self): return self.title
class Reservation(ModifiableModel): CANCELLED = 'cancelled' CONFIRMED = 'confirmed' DENIED = 'denied' REQUESTED = 'requested' STATE_CHOICES = ( (CANCELLED, _('cancelled')), (CONFIRMED, _('confirmed')), (DENIED, _('denied')), (REQUESTED, _('requested')), ) resource = models.ForeignKey('Resource', verbose_name=_('Resource'), db_index=True, related_name='reservations') begin = models.DateTimeField(verbose_name=_('Begin time')) end = models.DateTimeField(verbose_name=_('End time')) duration = pgfields.DateTimeRangeField(verbose_name=_('Length of reservation'), null=True, blank=True, db_index=True) comments = models.TextField(null=True, blank=True, verbose_name=_('Comments')) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), null=True, blank=True, db_index=True) state = models.CharField(max_length=16, choices=STATE_CHOICES, verbose_name=_('State'), default=CONFIRMED) approver = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Approver'), related_name='approved_reservations', null=True, blank=True) # access-related fields access_code = models.CharField(verbose_name=_('Access code'), max_length=32, null=True, blank=True) # EXTRA FIELDS START HERE event_subject = models.CharField(max_length=200, verbose_name=_('Event subject'), blank=True) event_description = models.TextField(verbose_name=_('Event description'), blank=True) number_of_participants = models.PositiveSmallIntegerField(verbose_name=_('Number of participants'), blank=True, null=True) host_name = models.CharField(verbose_name=_('Host name'), max_length=100, blank=True) # extra detail fields for manually confirmed reservations reserver_name = models.CharField(verbose_name=_('Reserver name'), max_length=100, blank=True) reserver_id = models.CharField(verbose_name=_('Reserver ID (business or person)'), max_length=30, blank=True) reserver_email_address = models.EmailField(verbose_name=_('Reserver email address'), blank=True) reserver_phone_number = models.CharField(verbose_name=_('Reserver phone number'), max_length=30, blank=True) reserver_address_street = models.CharField(verbose_name=_('Reserver address street'), max_length=100, blank=True) reserver_address_zip = models.CharField(verbose_name=_('Reserver address zip'), max_length=30, blank=True) reserver_address_city = models.CharField(verbose_name=_('Reserver address city'), max_length=100, blank=True) company = models.CharField(verbose_name=_('Company'), max_length=100, blank=True) billing_address_street = models.CharField(verbose_name=_('Billing address street'), max_length=100, blank=True) billing_address_zip = models.CharField(verbose_name=_('Billing address zip'), max_length=30, blank=True) billing_address_city = models.CharField(verbose_name=_('Billing address city'), max_length=100, blank=True) def _save_dt(self, attr, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ save_dt(self, attr, dt, self.resource.unit.time_zone) def _get_dt(self, attr, tz): return get_dt(self, attr, tz) @property def begin_tz(self): return self.begin @begin_tz.setter def begin_tz(self, dt): self._save_dt('begin', dt) def get_begin_tz(self, tz): return self._get_dt("begin", tz) @property def end_tz(self): return self.end @end_tz.setter def end_tz(self, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ self._save_dt('end', dt) def get_end_tz(self, tz): return self._get_dt("end", tz) def is_active(self): return self.end >= timezone.now() and self.state not in (Reservation.CANCELLED, Reservation.DENIED) def need_manual_confirmation(self): return self.resource.need_manual_confirmation def are_extra_fields_visible(self, user): if not self.need_manual_confirmation(): return True if not (user and user.is_authenticated()): return False return user == self.user or self.resource.is_admin(user) def can_view_access_code(self, user): return user == self.user or self.resource.can_view_access_codes(user) def set_state(self, new_state, user): if new_state == self.state: return if new_state == Reservation.CONFIRMED: self.approver = user if self.need_manual_confirmation(): self.send_reservation_confirmed_mail() elif self.state == Reservation.CONFIRMED: self.approver = None if new_state == Reservation.DENIED: self.send_reservation_denied_mail() elif new_state == Reservation.CANCELLED: if user != self.user: self.send_reservation_cancelled_mail() self.state = new_state self.save() class Meta: verbose_name = _("reservation") verbose_name_plural = _("reservations") def __str__(self): return "%s -> %s: %s" % (self.begin, self.end, self.resource) def clean(self, **kwargs): """ Check restrictions that are common to all reservations. If this reservation isn't yet saved and it will modify an existing reservation, the original reservation need to be provided in kwargs as 'original_reservation', so that it can be excluded when checking if the resource is available. """ if self.end <= self.begin: raise ValidationError(_("You must end the reservation after it has begun")) # Check that begin and end times are on valid time slots. opening_hours = self.resource.get_opening_hours(self.begin.date(), self.end.date()) for dt in (self.begin, self.end): days = opening_hours.get(dt.date(), []) day = next((day for day in days if day['opens'] is not None and day['opens'] <= dt <= day['closes']), None) if day and not is_valid_time_slot(dt, self.resource.min_period, day['opens']): raise ValidationError(_("Begin and end time must match time slots")) original_reservation = self if self.pk else kwargs.get('original_reservation', None) if self.resource.check_reservation_collision(self.begin, self.end, original_reservation): raise ValidationError(_("The resource is already reserved for some of the period")) if (self.end - self.begin) < self.resource.min_period: raise ValidationError(_("The minimum reservation length is %(min_period)s") % {'min_period': humanize_duration(self.min_period)}) if self.access_code: validate_access_code(self.access_code, self.resource.access_code_type) def send_reservation_mail(self, subject, template_name, extra_context=None, user=None): """ Stuff common to all reservation related mails. If user isn't given use self.user. """ if user: email_address = user.email else: if not (self.reserver_email_address or self.user): return email_address = self.reserver_email_address or self.user.email user = self.user language = user.get_preferred_language() if user else DEFAULT_LANG context = {'reservation': self} if extra_context: context.update(extra_context) send_respa_mail(email_address, subject, template_name, context, language) def send_reservation_requested_mail(self): self.send_reservation_mail(_("You've made a preliminary reservation"), 'reservation_requested') def send_reservation_requested_mail_to_officials(self): unit = self.resource.unit for user in get_users_with_perms(unit): if user.has_perm('can_approve_reservation', unit): self.send_reservation_mail(_('Reservation requested'), 'reservation_requested_official', user=user) def send_reservation_denied_mail(self): self.send_reservation_mail(_('Reservation denied'), 'reservation_denied') def send_reservation_confirmed_mail(self): self.send_reservation_mail(_('Reservation confirmed'), 'reservation_confirmed') def send_reservation_cancelled_mail(self): self.send_reservation_mail(_('Reservation cancelled'), 'reservation_cancelled') def send_reservation_created_with_access_code_mail(self): self.send_reservation_mail(_('Reservation created'), 'reservation_created_with_access_code') def save(self, *args, **kwargs): self.duration = DateTimeTZRange(self.begin, self.end, '[)') access_code_type = self.resource.access_code_type if not self.resource.is_access_code_enabled(): self.access_code = '' elif not self.access_code: self.access_code = generate_access_code(access_code_type) return super().save(*args, **kwargs) objects = ReservationQuerySet.as_manager()
class AbstractObservation(models.Model): """An observation is an act associated with a discrete time instant or period through which a quantity is assigned to a phenomenon (Property). It involves application of a specified procedure (Process), such as a sensor measurement or algorithm processing (e.g. hourly average).""" phenomenon_time_range = pgmodels.DateTimeRangeField( help_text="Datetime range when the observation was captured.", ) def phenomenon_time_from(self): return self.phenomenon_time_range.lower phenomenon_time_from.admin_order_field = 'phenomenon_time_range' @property def phenomenon_time_duration(self): delta = self.phenomenon_time_range.upper - self.phenomenon_time_range.lower return delta @property def phenomenon_time_duration_for_human(self): return format_delta(self.phenomenon_time_duration) phenomenon_time_duration_for_human.fget.short_description = "Phenomenon time duration" # phenomenon_time_to = models.DateTimeField( # help_text="End of the observation. If the observation was instant, " # "it is the same time as phenomenon_time.", # editable=False # ) observed_property = models.ForeignKey( Property, help_text="Phenomenon that was observed, e.g. air temperature.", related_name="%(app_label)s_%(class)s_related", editable=False, on_delete=models.DO_NOTHING, ) # NOTE: This field has to be overridden in child classes! # It needs to reference proper ForeignKey (Concrete Feature inherited from AbstractFeature) feature_of_interest = models.ForeignKey( AbstractFeature, help_text="Weather station where the observation was taken.", related_name="%(app_label)s_%(class)s_related", editable=False, on_delete=models.DO_NOTHING, ) procedure = models.ForeignKey( Process, help_text="Process used to generate the result, e.g. measurement or " "average.", related_name="%(app_label)s_%(class)s_related", editable=False, on_delete=models.DO_NOTHING, ) related_observations = models.ManyToManyField( 'self', help_text="Measured observations that were used to generate average " "observation, or vice versa.", editable=False, ) result = models.DecimalField( help_text="Numerical value of the measured phenomenon in units " "specified by Process.", max_digits=8, decimal_places=3, null=True, editable=False, ) time_slots = models.ForeignKey( TimeSlots, help_text="Time_slots used to calc aggregations", null=True, default=None, on_delete=models.DO_NOTHING, related_name="%(app_label)s_%(class)s_related", ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @property def result_for_human(self): if self.result is not None: res_str = "{}".format(self.result) else: reason = self.result_null_reason res_str = 'unknown because of ' + reason return res_str result_for_human.fget.short_description = 'Result' result_null_reason = models.CharField( help_text="Reason why result is null.", max_length=100, default='', ) class Meta: abstract = True get_latest_by = 'phenomenon_time_range' ordering = ['-phenomenon_time_range', 'feature_of_interest', 'procedure', 'observed_property'] unique_together = (('phenomenon_time_range', 'observed_property', 'feature_of_interest', 'procedure'),)
class Reservation(ModifiableModel): CREATED = 'created' CANCELLED = 'cancelled' CONFIRMED = 'confirmed' DENIED = 'denied' REQUESTED = 'requested' PENDING = 'pending' STATE_CHOICES = ( (CREATED, _('created')), (CANCELLED, _('cancelled')), (CONFIRMED, _('confirmed')), (DENIED, _('denied')), (REQUESTED, _('requested')), (PENDING, _('pending')), ) resource = models.ForeignKey('Resource', verbose_name=_('Resource'), db_index=True, related_name='reservations', on_delete=models.PROTECT) begin = models.DateTimeField(verbose_name=_('Begin time')) end = models.DateTimeField(verbose_name=_('End time')) duration = pgfields.DateTimeRangeField( verbose_name=_('Length of reservation'), null=True, blank=True, db_index=True) comments = models.TextField(null=True, blank=True, verbose_name=_('Comments')) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), null=True, blank=True, db_index=True, on_delete=models.PROTECT) state = models.CharField(max_length=16, choices=STATE_CHOICES, verbose_name=_('State'), default=CONFIRMED) approver = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Approver'), related_name='approved_reservations', null=True, blank=True, on_delete=models.SET_NULL) purchase = models.OneToOneField(Purchase, related_name="reservation", verbose_name=_('Purchase'), db_index=True, blank=True, null=True, on_delete=models.SET_NULL) staff_event = models.BooleanField(default=False, verbose_name=_('Staff event')) # access-related fields access_code = models.CharField(verbose_name=_('Access code'), max_length=32, null=True, blank=True) # EXTRA FIELDS START HERE event_subject = models.CharField(max_length=200, verbose_name=_('Event subject'), blank=True) manual_price = models.CharField(max_length=30, verbose_name=_('Manual price'), blank=True) event_description = models.TextField(verbose_name=_('Event description'), blank=True) number_of_participants = models.PositiveSmallIntegerField( verbose_name=_('Number of participants'), blank=True, null=True) participants = models.TextField(verbose_name=_('Participants'), blank=True) host_name = models.CharField(verbose_name=_('Host name'), max_length=100, blank=True) # extra detail fields for manually confirmed reservations reserver_name = models.CharField(verbose_name=_('Reserver name'), max_length=100, blank=True) reserver_id = models.CharField( verbose_name=_('Reserver ID (business or person)'), max_length=30, blank=True) reserver_email_address = models.EmailField( verbose_name=_('Reserver email address'), blank=True) reserver_phone_number = models.CharField( verbose_name=_('Reserver phone number'), max_length=30, blank=True) reserver_address_street = models.CharField( verbose_name=_('Reserver address street'), max_length=100, blank=True) reserver_address_zip = models.CharField( verbose_name=_('Reserver address zip'), max_length=30, blank=True) reserver_address_city = models.CharField( verbose_name=_('Reserver address city'), max_length=100, blank=True) company = models.CharField(verbose_name=_('Company'), max_length=100, blank=True) billing_address_street = models.CharField( verbose_name=_('Billing address street'), max_length=100, blank=True) billing_address_zip = models.CharField( verbose_name=_('Billing address zip'), max_length=30, blank=True) billing_address_city = models.CharField( verbose_name=_('Billing address city'), max_length=100, blank=True) # If the reservation was imported from another system, you can store the original ID in the field below. origin_id = models.CharField(verbose_name=_('Original ID'), max_length=50, editable=False, null=True) objects = ReservationQuerySet.as_manager() class Meta: verbose_name = _("reservation") verbose_name_plural = _("reservations") ordering = ('id', ) def _save_dt(self, attr, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ save_dt(self, attr, dt, self.resource.unit.time_zone) def _get_dt(self, attr, tz): return get_dt(self, attr, tz) @property def begin_tz(self): return self.begin @begin_tz.setter def begin_tz(self, dt): self._save_dt('begin', dt) def get_begin_tz(self, tz): return self._get_dt("begin", tz) @property def end_tz(self): return self.end @end_tz.setter def end_tz(self, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ self._save_dt('end', dt) def get_end_tz(self, tz): return self._get_dt("end", tz) def is_active(self): return self.end >= timezone.now() and self.state not in ( Reservation.CANCELLED, Reservation.DENIED) def is_own(self, user): if not (user and user.is_authenticated): return False return user == self.user def need_manual_confirmation(self): return self.resource.need_manual_confirmation def are_extra_fields_visible(self, user): # the following logic is used also implemented in ReservationQuerySet # so if this is changed that probably needs to be changed as well if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_access_code(self, user): if self.is_own(user): return True return self.resource.can_view_access_codes(user) def set_purchase(self, new_purchase): self.purchase = new_purchase self.save() def set_state(self, new_state, user): # Make sure it is a known state assert new_state in (Reservation.REQUESTED, Reservation.CONFIRMED, Reservation.DENIED, Reservation.CANCELLED) old_state = self.state if new_state == old_state: if old_state == Reservation.CONFIRMED: reservation_modified.send(sender=self.__class__, instance=self, user=user) return if new_state == Reservation.CONFIRMED: self.approver = user reservation_confirmed.send(sender=self.__class__, instance=self, user=user) elif old_state == Reservation.CONFIRMED: self.approver = None # self.send_messages(new_state, user) self.state = new_state self.save() def send_messages(self, new_state, user): # Notifications # Someone needs to rewrite this spaghetti monster if new_state == Reservation.REQUESTED: self.send_reservation_requested_mail() # self.send_reservation_requested_mail_to_officials() elif new_state == Reservation.CONFIRMED: # If a payment is required, await for purchase to complete before sending confirmation mail if self.resource.ceepos_payment_required and self.resource.product_code and not self.purchase and self.resource.need_manual_confirmation and not self.staff_event: if self.manual_price != '': p = Purchase.objects.create(purchase_code = self.resource.product_code,\ price_vat = float(self.manual_price)) else: p = Purchase.objects.create(purchase_code = self.resource.product_code, \ price_vat = float(self.resource.min_price_per_hour) * (self.end - self.begin).total_seconds() / 3600) p.request_payment() self.set_purchase(p) self.send_payment_email() return elif self.resource.ceepos_payment_required and not self.resource.need_manual_confirmation: return elif not self.need_manual_confirmation(): self.send_reservation_confirmed_mail() if self.need_manual_confirmation(): self.send_reservation_confirmed_mail() elif self.resource.is_access_code_enabled(): self.send_reservation_created_with_access_code_mail() elif new_state == Reservation.DENIED: self.send_reservation_denied_mail() elif new_state == Reservation.CANCELLED: if user != self.user: self.send_reservation_cancelled_mail() reservation_cancelled.send(sender=self.__class__, instance=self, user=user) def can_modify(self, user): if not user: return False # reservations that need manual confirmation and are confirmed cannot be # modified or cancelled without reservation approve permission cannot_approve = not self.resource.can_approve_reservations(user) if self.need_manual_confirmation( ) and self.state == Reservation.CONFIRMED and cannot_approve: return False return self.user == user or self.resource.can_modify_reservations(user) def can_add_comment(self, user): if self.is_own(user): return True return self.resource.can_access_reservation_comments(user) def can_view_field(self, user, field): if field not in RESERVATION_EXTRA_FIELDS: return True if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_catering_orders(self, user): if self.is_own(user): return True return self.resource.can_view_catering_orders(user) def format_time(self): tz = self.resource.unit.get_tz() begin = self.begin.astimezone(tz) end = self.end.astimezone(tz) return format_dt_range(translation.get_language(), begin, end) def __str__(self): if self.state != Reservation.CONFIRMED: state_str = ' (%s)' % self.state else: state_str = '' return "%s: %s%s" % (self.format_time(), self.resource, state_str) def clean(self, **kwargs): """ Check restrictions that are common to all reservations. If this reservation isn't yet saved and it will modify an existing reservation, the original reservation need to be provided in kwargs as 'original_reservation', so that it can be excluded when checking if the resource is available. """ if self.end <= self.begin: raise ValidationError( _("You must end the reservation after it has begun")) # Check that begin and end times are on valid time slots. opening_hours = self.resource.get_opening_hours( self.begin.date(), self.end.date()) for dt in (self.begin, self.end): days = opening_hours.get(dt.date(), []) day = next((day for day in days if day['opens'] is not None and day['opens'] <= dt <= day['closes']), None) if day and not is_valid_time_slot(dt, self.resource.min_period, day['opens']): raise ValidationError( _("Begin and end time must match time slots")) original_reservation = self if self.pk else kwargs.get( 'original_reservation', None) if self.resource.check_reservation_collision(self.begin, self.end, original_reservation): raise ValidationError( _("The resource is already reserved for some of the period")) if (self.end - self.begin) < self.resource.min_period: raise ValidationError( _("The minimum reservation length is %(min_period)s") % {'min_period': humanize_duration(self.min_period)}) if self.access_code: validate_access_code(self.access_code, self.resource.access_code_type) def get_notification_context(self, language_code, user=None): if not user: user = self.user with translation.override(language_code): reserver_name = self.reserver_name if not reserver_name and self.user and self.user.get_display_name( ): reserver_name = self.user.get_display_name() context = { 'resource': self.resource.name, 'begin': localize_datetime(self.begin), 'end': localize_datetime(self.end), 'begin_dt': self.begin, 'end_dt': self.end, 'time_range': self.format_time(), 'number_of_participants': self.number_of_participants, 'host_name': self.host_name, 'reserver_name': reserver_name, 'reserver_phone_number': self.reserver_phone_number, 'reserver_email_address': self.user, 'event_subject': self.event_subject, 'manual_price': self.manual_price, 'reservation_link': settings.ROOT_HOST + '/admin/resources/reservation/' + str(self.id) + '/change/', } if self.resource.need_manual_confirmation: context['manual_confirmation'] = Resource._meta.get_field( 'need_manual_confirmation').verbose_name if self.purchase and self.purchase.payment_address: context['purchase_link'] = self.purchase.payment_address else: context['purchase_link'] = "Purchase object: {}".format( str(self.purchase)) if self.resource.unit: context['unit'] = self.resource.unit.name if self.resource.responsible_contact_info: context[ 'responsible_contact_info'] = self.resource.responsible_contact_info if self.can_view_access_code(user) and self.access_code: context['access_code'] = self.access_code if self.resource.reservation_confirmed_notification_extra: context[ 'extra_content'] = self.resource.reservation_confirmed_notification_extra + ' - ' return context def send_reservation_mail(self, notification_type, user=None): """ Stuff common to all reservation related mails. If user isn't given use self.user. """ if user: email_address = user.email else: if not (self.reserver_email_address or self.user): return email_address = self.reserver_email_address or self.user.email user = self.user language = user.get_preferred_language() if user else DEFAULT_LANG context = self.get_notification_context(language) try: rendered_notification = render_notification_template( notification_type, context, language) except NotificationTemplateException as e: logger.error(e, exc_info=True, extra={'user': user.uuid}) return send_respa_mail(email_address, rendered_notification['subject'], rendered_notification['body']) if notification_type == "reservation_requested": notification_type = "reservation_requested_official" if notification_type == "reservation_payment_successful": notification_type = "reservation_payment_successful_official" if notification_type == "reservation_requested_payment": return try: rendered_notification = render_notification_template( notification_type, context, language) except NotificationTemplateException as e: logger.error(e, exc_info=True, extra={'user': user.uuid}) return owner_address = self.resource.owner_email send_respa_mail(owner_address, rendered_notification['subject'], rendered_notification['body']) def send_reservation_requested_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_REQUESTED) def send_reservation_requested_mail_to_officials(self): notify_users = self.resource.get_users_with_perm( 'can_approve_reservation') if len(notify_users) > 100: raise Exception("Refusing to notify more than 100 users (%s)" % self) for user in notify_users: self.send_reservation_mail( NotificationType.RESERVATION_REQUESTED_OFFICIAL, user=user) def send_reservation_denied_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_DENIED) def send_reservation_confirmed_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_CONFIRMED) def send_reservation_cancelled_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_CANCELLED) def send_reservation_created_with_access_code_mail(self): self.send_reservation_mail( NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE) def send_payment_email(self): self.send_reservation_mail( NotificationType.RESERVATION_REQUESTED_PAYMENT) def send_payment_success_mail(self): self.send_reservation_mail( NotificationType.RESERVATION_PAYMENT_SUCCESSFUL) def send_payment_failed_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_FAILED_PAYMENT) def __init__(self, *args, **kwargs): super(Reservation, self).__init__(*args, **kwargs) self.old_myStatus = self.state def save(self, *args, **kwargs): self.duration = DateTimeTZRange(self.begin, self.end, '[)') access_code_type = self.resource.access_code_type if not self.resource.is_access_code_enabled(): self.access_code = '' elif not self.access_code: self.access_code = generate_access_code(access_code_type) if self.old_myStatus != self.state: self.old_myStatus = self.state self.send_messages(self.state, self.approver) return super().save(*args, **kwargs)