Code source de resax.models

# coding: utf-8

from __future__ import unicode_literals

import collections
import swapper

from .utils import iter_daterange
from datetime import datetime
from datetime import time
from datetime import timedelta
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.db import transaction
from django.db.models import Sum
from django.utils import six
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import localtime
from django.utils.timezone import make_aware
from django.utils.translation import ugettext_lazy as _

#
# Swappable models helpers
#

class ModelMetaclass(type):
    def __getitem__(self, name):
        return swapper.get_model_name('resax', name)

    def __getattr__(self, name):
        if name[:1] == '_':
            raise AttributeError
        return swapper.load_model('resax', name)

@six.add_metaclass(ModelMetaclass)
class Model:
    pass

#
# Domain-specific models
#

@python_2_unicode_compatible
[docs]class AbstractOrganisation(models.Model): """ Represents an organisation using the API. """ #: Name of the organisation name = models.CharField(_("name"), max_length=255, unique=True) #: Flag indicating if the organisation was deleted deleted = models.BooleanField(_("deleted"), default=False) class Meta: abstract = True verbose_name = _("organisation") verbose_name_plural = _("organisations") def __str__(self): return self.name @transaction.atomic
[docs] def add_user(self): """ Adds a new member (user) to the organisation :rtype: User """ return self.users.create()
@transaction.atomic
[docs] def add_resource_type(self, name, resources=None): """ Ajoute un nouveau type de ressource à l'organisation. Si l'argument *resources* est spécifié, il doit être un dictionnaire ayant pour clé le nom de la ressource et pour valeur sa quantité en stock. :param name: nom du type de ressource :type name: str :param resources: dictionnaire facultatif contenant les ressources à créer de ce type. Chaque clé doit correspondre au nom de la ressource, et chaque valeur doit indiquer la quantité disponible en stock :type resources: dict :rtype: ResourceType """ if resources is None: resources = {} resource_type = Model.ResourceType(name=name) resource_type.organisation = self resource_type.full_clean() resource_type.save(force_insert=True) for name, stock in resources.items(): resource_type.add_resource(name, stock) return resource_type
@transaction.atomic
[docs] def add_activity(self, name, stock=1, resources=None): """ Ajoute une activité à l'organisation. :param name: nom de l'activité :type name: str :param stock: nombre de places disponibles pour l'activité :type stock: int :param resources: dictionnaire facultatif contenant les ressources à ajouter à cette activité. Chaque clé doit correspondre à une ressource, et chaque valeur doit indiquer la quantité requise pour l'activité :type resources: dict :rtype: Activity """ if resources is None: resources = {} activity = Model.Activity(name=name, stock=stock) activity.organisation = self activity.full_clean() activity.save(force_insert=True) for resource, quantity in resources.items(): if resource.organisation != self: raise ValidationError(_("Resource %s doesn't belong to this organisation.") % resource) activity.add_resource(resource, quantity) return activity
@transaction.atomic
[docs] def add_reservation_type(self, name, resources=None): """ Ajoute un type de réservation à l'organisation. Si l'argument *resources* est spécifié, il doit être une liste contenant les ressources autorisées pour ce type de réservation. :param name: nom du type de réservation :type name: str :param resources: liste facultative contenant les ressources autorisées pour ce type de réservation :type resources: list :rtype: ReservationType """ if resources is None: resources = [] if not isinstance(resources, collections.Iterable): resources = [resources] reservation_type = Model.ReservationType(name=name) reservation_type.organisation = self reservation_type.full_clean() reservation_type.save(force_insert=True) for resource in resources: if resource.organisation != self: raise ValidationError(_("Resource %s doesn't belong to this organisation.") % resource) reservation_type.add_resource(resource) return reservation_type
[docs]class Organisation(AbstractOrganisation): class Meta(AbstractOrganisation.Meta): swappable = swapper.swappable_setting('resax', 'Organisation')
@python_2_unicode_compatible
[docs]class AbstractUser(models.Model): """ Représente un utilisateur (membre) d'une organisation. """ #: L'organisation à laquelle appartient l'utilisateur organisation = models.ForeignKey(Model['Organisation'], on_delete=models.CASCADE, verbose_name=_("organisation"), related_name='users') #: Liste des évènements réservés par l'utilisateur events = models.ManyToManyField(Model['Event'], through=Model['Reservation'], verbose_name=_("events"), related_name='users') class Meta: abstract = True verbose_name = _("user") verbose_name_plural = _("users") def __str__(self): return "User %s" % self.pk def check_reservation_params(self, reservation_object, date_start, date_stop): if date_start < timezone.now(): raise ValidationError(_("The starting date must be greater than the current date")) if date_stop <= date_start: raise ValidationError(_("The ending date must be greater than the starting date")) if reservation_object.organisation != self.organisation: raise ValidationError(_("This doesn't belong to the organisation of the chosen reservation object")) def get_upcoming_reservations(self): current_date = timezone.now() return self.reservations.filter( event__date_stop__gt=current_date, ).order_by('event__date_start') def get_past_reservations(self): current_date = timezone.now() return self.reservations.filter( event__date_stop__lte=current_date, ).order_by('-event__date_start') @transaction.atomic
[docs] def book_event(self, event, quantity=1): """ Réserve *quantity* places pour l'évènement *event*. :param event: évènement à réserver :type event: Event :param quantity: nombre de places à réserver :type quantity: int :rtype: Reservation """ return event.book(self, quantity)
@transaction.atomic
[docs] def book_resources(self, reservation_type, date_start, date_stop, resources=None): r""" Crée une réservation flexible. :param reservation_type: type de réservation :type reservation_type: ReservationType :param date_start: date de début de la réservation :param date_stop: date de fin de la réservation :param resources: dictionnaire facultatif contenant les ressources à réserver. Ce dictionnaire doit être composé d'un ensemble de clés et de valeurs, où chaque clé réprésente la ressource à réserver, et chaque valeur la quantité demandée :type resources: dict :rtype: FlexiReservation """ if resources is None: resources = {} self.check_reservation_params(reservation_type, date_start, date_stop) allowed_resources = reservation_type.resources.filter(pk__in=[r.pk for r in resources.keys()]).only('pk') allowed_resources = allowed_resources.select_for_update() for resource, quantity in resources.items(): if resource not in allowed_resources: raise ValidationError(_("Resource %s is not avaible for this reservation type") % resource) available_stock = resource.get_available_stock(date_start, date_stop) if available_stock < quantity: raise ValidationError(_("Not enough stock for resource %s") % resource) event = Model.Event(date_start=date_start, date_stop=date_stop, stock=1) event.save(force_insert=True) reservation = Model.FlexiReservation(reservation_type=reservation_type, user=self, event=event) reservation.save(force_insert=True) event.full_clean() reservation.full_clean() for resource, quantity in resources.items(): reservation.add_resource(resource, quantity) return reservation
[docs]class User(AbstractUser): class Meta(AbstractUser.Meta): swappable = swapper.swappable_setting('resax', 'User')
@python_2_unicode_compatible
[docs]class AbstractResourceType(models.Model): """ Représente un type de ressource. """ #: Organisation à laquelle appartient le type de ressource organisation = models.ForeignKey(Model['Organisation'], on_delete=models.CASCADE, verbose_name=_("organisation"), related_name='resource_types') #: Nom du type de ressource name = models.CharField(_("name"), max_length=255) #: Drapeau indiquant que le type de ressource est supprimé deleted = models.BooleanField(_("deleted"), default=False) class Meta: abstract = True verbose_name = _("resource type") verbose_name_plural = _("resource types") unique_together = ('organisation', 'name') def __str__(self): return self.name @transaction.atomic def add_resource(self, name, stock=1): resource = Model.Resource(name=name, stock=stock) resource.resource_type = self resource.full_clean() resource.save(force_insert=True) return resource
[docs]class ResourceType(AbstractResourceType): class Meta(AbstractResourceType.Meta): swappable = swapper.swappable_setting('resax', 'ResourceType')
@python_2_unicode_compatible
[docs]class AbstractResource(models.Model): """ Représente une ressource. """ #: Type de la ressource resource_type = models.ForeignKey(Model['ResourceType'], on_delete=models.CASCADE, verbose_name=_("resource type"), related_name='resources') #: Nom de la ressource name = models.CharField(_("name"), max_length=255) #: Sotck disponible pour la ressource. 0 signifie que la stock est illimité. stock = models.PositiveIntegerField(_("stock"), default=0) #: Drapeau indiquant que la ressource est supprimée deleted = models.BooleanField(_("deleted"), default=False) class Meta: abstract = True verbose_name = _("resource") verbose_name_plural = _("resources") unique_together = ('resource_type', 'name') def __str__(self): return "%s" % self.name @property def organisation(self): return self.resource_type.organisation
[docs] def get_available_stock(self, date_start, date_stop, exclude_event=None): """ Retour la quantité disponible de la ressource sur la période entre les dates *date_start* et *date_stop* spécifiées. Si *exclude_event* est spécifié, cet évènement n'est pas pris en compte dans les résultats. :param date_start: date de début de disponibilité recherchée pour la ressource :param date_stop: date de fin de disponibilité recherchée pour la ressource :param exclude_event: évènement facultatif à ne pas prendre en compte pour le caclul des résultats :type exclude_event: Event :rtype: int """ if self.stock == 0: return 0 flexi_stock = self.flexi_reservation_resources.filter( flexi_reservation__event__date_start__lt=date_stop, flexi_reservation__event__date_stop__gt=date_start, ).exclude( flexi_reservation__event__pk=(exclude_event.pk if exclude_event else None), ).aggregate(v=Sum('quantity'))['v'] or 0 activity_stock = self.activity_resources.filter( activity__events__date_start__lt=date_stop, activity__events__date_stop__gt=date_start, ).exclude( activity__events__pk=(exclude_event.pk if exclude_event else None), ).aggregate(v=Sum('quantity'))['v'] or 0 # TODO: test this with multiple events per ActivityResource return self.stock - (flexi_stock + activity_stock)
@transaction.atomic def lock(self): self.__class__.objects.select_for_update().filter(pk=self.pk).exists() @transaction.atomic
[docs] def set_stock(self, new_stock): """ Redéfinit la quantité en stock de la ressource. :param new_stock: quantité en stock de la ressource ; 0 si illimitée :type new_stock: int """ if self.stock == new_stock: return self.lock() # preserves ActivityResource.quantity <= Resource.stock TODO: preserves other constraints too! self.stock = new_stock self.full_clean() # TODO: implement a clean() method that checks all constraints (for Activity, Event, etc) self.save(update_fields=['stock'])
[docs]class Resource(AbstractResource): class Meta(AbstractResource.Meta): swappable = swapper.swappable_setting('resax', 'Resource')
@python_2_unicode_compatible
[docs]class AbstractEvent(models.Model): """ Représentation d'un évènement dans le calendrier. """ #: Activité associée à cet évènement (faculatif) activity = models.ForeignKey(Model['Activity'], on_delete=models.CASCADE, verbose_name=_("activity"), related_name='events', null=True, blank=True) #: Planning associé à cet évènement (facultatif) planning = models.ForeignKey(Model['Planning'], on_delete=models.CASCADE, verbose_name=_("planning"), related_name='events', null=True, blank=True) #: Date et heure de début de l'évènement date_start = models.DateTimeField(_("date_start"), db_index=True) #: Date et heure de fin de l'évènement date_stop = models.DateTimeField(_("date_stop"), db_index=True) #: Nombre de réservations possibles pour cet évènement. 0 signifie réservations illimitées stock = models.PositiveIntegerField(_("stock"), default=0) class Meta: abstract = True verbose_name = _("event") verbose_name_plural = _("events") def __str__(self): event_name = "" if self.activity: event_name = self.activity else: try: if self.flexi_reservation: event_name = self.flexi_reservation.reservation_type.name except Model.FlexiReservation.DoesNotExist: pass return "Event %s %s (%s to %s)" % (self.pk, event_name, self.date_start, self.date_stop) @property def duration(self): return self.date_stop - self.date_start @property def is_flexible(self): try: flexi_reservation = bool(self.flexi_reservation) except Model.FlexiReservation.DoesNotExist: flexi_reservation = False if self.activity and not flexi_reservation: return False elif not self.activity and flexi_reservation: return True else: raise NotImplementedError @property def resources(self): if self.activity is None: try: return self.flexi_reservation.resources.all() except Model.FlexiReservation.DoesNotExist: return Model.Resource.objects.none() else: return self.activity.resources.all() @property def activity_resources(self): if self.activity is None: return Model.ActivityResource.objects.none() else: return self.activity.activity_resources.all() @property def flexi_reservation_resources(self): try: return self.flexi_reservation.flexi_reservation_resources.all() except Model.FlexiReservation.DoesNotExist: return Model.FlexiReservationResource.objects.none() @property def used_resources(self): if self.activity is None: return self.flexi_reservation_resources else: return self.activity_resources def get_available_seats(self, exclude_event=None): excluded_pk = exclude_event.pk if exclude_event else None if self.stock > 0: taken_seats = self.reservations.exclude(pk=excluded_pk).aggregate(v=Sum('quantity'))['v'] or 0 return self.stock - taken_seats else: return float('inf') def _clean_stock(self): if self.get_available_seats() < 0: raise ValidationError(_("Event's stock can not be inferior to the number of seats already reserved")) def clean(self): if self.date_stop <= self.date_start: raise ValidationError(_("Event's ending date must be greater than the starting date")) if self.planning and self.planning.activity != self.activity: raise ValidationError(_("Specified planning isn't associated to the activity of this event")) self._clean_stock() try: if self.flexi_reservation and self.activity: raise ValidationError(_("An event can not simultaneously be linked to an activity and a flexible reservation")) except Model.FlexiReservation.DoesNotExist: if not self.activity: raise ValidationError(_("An event has to be associated to an activity or to a flexible reservation")) for ur in self.used_resources.exclude(resource__stock=0): available_stock = ur.resource.get_available_stock(self.date_start, self.date_stop, self) if available_stock < ur.quantity: raise ValidationError(_("Stock of resource %s is overused") % ur.resource, code='stock') @transaction.atomic def lock(self): self.__class__.objects.select_for_update().filter(pk=self.pk).exists() @transaction.atomic
[docs] def set_stock(self, new_stock): """ Redéfinit le nombre de places disponibles pour l'évènement. :param new_stock: nombre de places disponibles pour l'évènement ; 0 si illimité :type new_stock: int """ if self.stock == new_stock: return self.lock() # preserves self.stock >= self.reservations.aggregate(v=Sum('quantity'))['v'] self.stock = new_stock self._clean_stock() self.save(update_fields=['stock'])
@transaction.atomic
[docs] def book(self, user, quantity=1): """ Réserve cet évènement pour un utilisateur. :param user: utilisateur à associer à la réservation :type user: User :param quantity: nombre de places à réserver :type quantity: int :rtype: Reservation """ self.lock() available_seats = self.get_available_seats() if available_seats < quantity: raise ValidationError(_("There are not enough seats left for this event")) reservation = Model.Reservation(user=user, quantity=quantity) reservation.event = self reservation.full_clean() reservation.save(force_insert=True) return reservation
[docs]class Event(AbstractEvent): class Meta(AbstractEvent.Meta): swappable = swapper.swappable_setting('resax', 'Event')
@python_2_unicode_compatible
[docs]class AbstractReservation(models.Model): """ Représente une réservation pour un évènement. """ #: L'évènement réservé event = models.ForeignKey(Model['Event'], on_delete=models.CASCADE, verbose_name=_("event"), related_name='reservations') #: L'utilisateur ayant réservé l'évènement user = models.ForeignKey(Model['User'], on_delete=models.CASCADE, verbose_name=_("user"), related_name='reservations') #: Nombre de places réservées quantity = models.IntegerField(_("quantity"), validators=[MinValueValidator(1)], default=0) class Meta: abstract = True verbose_name = _("reservation") verbose_name_plural = _("reservations") def __str__(self): return "Reservation %s" % self.pk def clean(self): if self.event.get_available_seats() < self.quantity: raise ValidationError(_("Not enough seats left for this event"))
[docs]class Reservation(AbstractReservation): class Meta(AbstractReservation.Meta): swappable = swapper.swappable_setting('resax', 'Reservation')
@python_2_unicode_compatible
[docs]class AbstractReservationType(models.Model): """ Type de réservation. Un type de réservation permet à un utilisateur de réserver les ressources autorisées. """ #: Liste des ressources autorisées pour ce type de réservation resources = models.ManyToManyField(Model['Resource'], verbose_name=_("resources"), related_name='reservation_type') #: L'organisation à laquelle appartient ce type de réservation organisation = models.ForeignKey(Model['Organisation'], on_delete=models.CASCADE, verbose_name=_("organisation"), related_name='reservation_types') #: Nom du type de réservation name = models.CharField(_("name"), max_length=255) class Meta: abstract = True verbose_name = _("reservation type") verbose_name_plural = _("reservation types") unique_together = ('organisation', 'name') def __str__(self): return self.name @transaction.atomic def lock(self): self.__class__.objects.select_for_update().filter(pk=self.pk).exists() @transaction.atomic def add_resource(self, resource): self.lock() # preserves uniqueness of (ReservationTypeResource.resource_id, ReservationTypeResource.reservationtype_id) try: # if the resource is already associated, we don't do anything self.resources.get(pk=resource.pk) except Model.Resource.DoesNotExist: self.resources.add(resource)
[docs]class ReservationType(AbstractReservationType): class Meta(AbstractReservationType.Meta): swappable = swapper.swappable_setting('resax', 'ReservationType')
@python_2_unicode_compatible
[docs]class AbstractFlexiReservation(models.Model): """ Réservation flexible. Une réservation est dite “flexible” lorsqu'elle ne vient pas se greffer sur un évènement existant, mais lorsqu'elle crée elle-même un évènement. Cet évènement n'est alors pas associé à une activité ; les ressouces réservées sont directement spécifiées par l'utilisateur. """ #: L'utilisateur ayant fait la réservation user = models.ForeignKey(Model['User'], on_delete=models.CASCADE, verbose_name=_("user"), related_name='flexi_reservations') #: Type de réservation reservation_type = models.ForeignKey(Model['ReservationType'], on_delete=models.CASCADE, verbose_name=_("reservation type"), related_name='flexi_reservations') #: Ressources réservées, avec les quantités requises resources = models.ManyToManyField(Model['Resource'], through=Model['FlexiReservationResource'], verbose_name=_("resources"), related_name='flexi_reservations') #: L'évènement créé pour honorer cette réservation event = models.OneToOneField(Model['Event'], on_delete=models.CASCADE, verbose_name=_("event"), related_name='flexi_reservation') class Meta: abstract = True verbose_name = _("flexible reservations") verbose_name_plural = _("flexible reservations") def __str__(self): return "Flexible reservation %s" % self.pk @transaction.atomic def lock(self): self.__class__.objects.select_for_update().filter(pk=self.pk).exists() @transaction.atomic def add_resource(self, resource, quantity): resource.lock() # preserves FlexiReservationResource.quantity <= Resource.stock self.lock() # preserves uniqueness of (FlexiReservationResource.resource, FlexiReservationResource.flexi_reservation) if quantity > resource.stock: raise ValidationError(_("Quantity can't be greater than the available stock")) try: # if the resource is already associated, we merge quantities ar = self.flexi_reservation_resources.get(resource=resource) ar.quantity += quantity ar.full_clean() ar.save(update_fields=['quantity']) except Model.FlexiReservationResource.DoesNotExist: ar = self.flexi_reservation_resources.create(resource=resource, quantity=quantity) return ar
[docs]class FlexiReservation(AbstractFlexiReservation): class Meta(AbstractFlexiReservation.Meta): swappable = swapper.swappable_setting('resax', 'FlexiReservation')
@python_2_unicode_compatible
[docs]class AbstractFlexiReservationResource(models.Model): """ Ressource réservée par une réservation dite “flexible”. """ #: Réservation “flexible” associée flexi_reservation = models.ForeignKey(Model['FlexiReservation'], on_delete=models.CASCADE, verbose_name=_("reservation"), related_name='flexi_reservation_resources') #: Ressource réservée resource = models.ForeignKey(Model['Resource'], on_delete=models.CASCADE, verbose_name=_("resource"), related_name='flexi_reservation_resources') #: Quantité de la ressource requises quantity = models.IntegerField(_("quantity"), validators=[MinValueValidator(-1)], default=0) class Meta: abstract = True verbose_name = _("flexible reservation resource") verbose_name_plural = _("flexible reservation resources") def __str__(self): return "Flexible reservation resource %s" % self.pk
[docs]class FlexiReservationResource(AbstractFlexiReservationResource): class Meta(AbstractFlexiReservationResource.Meta): swappable = swapper.swappable_setting('resax', 'FlexiReservationResource')
@python_2_unicode_compatible
[docs]class AbstractActivity(models.Model): """ Activité de l'organisation. """ #: Liste des ressources nécessaires pour l'activité resources = models.ManyToManyField(Model['Resource'], through=Model['ActivityResource'], verbose_name=_("resources"), related_name='activities') #: L'organisation à laquelle appartient l'activité organisation = models.ForeignKey(Model['Organisation'], on_delete=models.CASCADE, verbose_name=_("organisation"), related_name='activities') #: Nom de l'activité name = models.CharField(_("name"), max_length=255) #: Nombre de places disponibles pour cette activité stock = models.PositiveIntegerField(_("stock"), default=0) #: Drapeau indiquant que l'activité est supprimée deleted = models.BooleanField(_("deleted"), default=False) class Meta: abstract = True verbose_name = _("activity") verbose_name_plural = _("activities") def __str__(self): return self.name
[docs] def get_events_of_the_day(self, date=None): """ Retourne tous les évènements correspondants à cette activité pour la date donnée, ou la date actuelle. :param date: date du jour :type date: datetime :rtype: QuerySet """ if not date: date = timezone.now() return self.events.filter(date_start__day=date.day).order_by('date_start').all()
@transaction.atomic def lock(self): self.__class__.objects.select_for_update().filter(pk=self.pk).exists() @transaction.atomic def lock_resources(self): self.resources.select_for_update().exists() @transaction.atomic def add_resource(self, resource, quantity): resource.lock() # preserves ActivityResource.quantity <= Resource.stock self.lock() # preserves uniqueness of (ActivityResource.resource, ActivityResource.activity) if quantity > resource.stock: raise ValidationError(_("Quantity can't be greater than the available stock")) try: # if the resource is already associated, we merge quantities ar = self.activity_resources.get(resource=resource) ar.quantity += quantity ar.full_clean() ar.save(update_fields=['quantity']) except Model.ActivityResource.DoesNotExist: ar = self.activity_resources.create(resource=resource, quantity=quantity) return ar @transaction.atomic def add_event(self, date_start, date_stop, stock=None, planning=None): if stock is None: stock = self.stock self.lock_resources() # preserves Resource.get_available_stock(date_start, date_stop) >= ActivityResource.quantity event = Model.Event(date_start=date_start, date_stop=date_stop, stock=stock, planning=planning) event.activity = self event.full_clean() event.save(force_insert=True)
[docs]class Activity(AbstractActivity): class Meta(AbstractActivity.Meta): swappable = swapper.swappable_setting('resax', 'Activity')
@python_2_unicode_compatible
[docs]class AbstractActivityResource(models.Model): """ Ressource d'activité. """ #: Ressource requise resource = models.ForeignKey(Model['Resource'], on_delete=models.CASCADE, verbose_name=_("resource"), related_name='activity_resources') #: Activité correspondante activity = models.ForeignKey(Model['Activity'], on_delete=models.CASCADE, verbose_name=_("activity"), related_name='activity_resources') #: Quantité de la ressource requise pour l'activité ; la valeur spéciale -1 consomme la totalité de la ressource quantity = models.IntegerField(_("quantity"), validators=[MinValueValidator(-1)], default=0) class Meta: abstract = True verbose_name = _("activity resource") verbose_name_plural = _("activity resources") unique_together = ('resource', 'activity') def __str__(self): return "Activity resource : (%s, %s, %s)" % (self.activity, self.resource, self.quantity) def clean(self): if self.quantity > self.resource.stock: raise ValidationError(_("Required quantity can't be greater than the available stock of the resource")) @transaction.atomic def set_quantity(self, new_quantity): self.resource.lock() # preserves ActivityResource.quantity <= Resource.stock if self.quantity == new_quantity: return self.quantity = new_quantity self.full_clean() self.save(update_fields=['quantity'])
[docs]class ActivityResource(AbstractActivityResource): class Meta(AbstractActivityResource.Meta): swappable = swapper.swappable_setting('resax', 'ActivityResource')
@python_2_unicode_compatible
[docs]class AbstractPlanning(models.Model): """ Planning de l'activité. """ #: Activité planifiée activity = models.ForeignKey(Model['Activity'], on_delete=models.CASCADE, verbose_name=_("activity"), related_name='plannings') #: Jours de la semaine programmés pour l'activité on_day0 = models.BooleanField(_("monday"), default=False) on_day1 = models.BooleanField(_("tuesday"), default=False) on_day2 = models.BooleanField(_("wednesday"), default=False) on_day3 = models.BooleanField(_("thursday"), default=False) on_day4 = models.BooleanField(_("friday"), default=False) on_day5 = models.BooleanField(_("saturday"), default=False) on_day6 = models.BooleanField(_("sunday"), default=False) #: Date et heure de début du premier évènement planifié time_start = models.DateTimeField(_("time start")) #: Date et heure de fin du premier évènement planifié time_stop = models.DateTimeField(_("time stop")) #: Date de fin de l'activité date_stop = models.DateTimeField(_("date stop"), null=True, blank=True) class Meta: abstract = True verbose_name = _("planning") verbose_name_plural = _("plannings") def __str__(self): return "Planning %s" % self.pk def gen_future_event(self, date): event = Model.Event() event.activity = self.activity event.planning = self event.stock = self.activity.stock event.date_start = datetime.combine(date, self.time_start.timetz()) event.date_stop = datetime.combine(date, self.time_stop.timetz()) # daylight saving time correction event.date_start += event.date_start.dst() - localtime(event.date_start).dst() event.date_stop += event.date_stop.dst() - localtime(event.date_stop).dst() if event.date_start > event.date_stop: event.date_stop += timedelta(days=1) return event def activate_days(self, days='0123456'): for d in range(7): setattr(self, 'on_day%d' % d, str(d) in days) @transaction.atomic def create_future_events(self, date_stop=None): if not self.date_stop and not date_stop: raise ValidationError(_("Stop date should be specified.")) date_stop = min(filter(None, [date_stop, self.date_stop])) current_date = max(self.time_start, timezone.now()) last_event = self.events.order_by('-date_start').first() if last_event: current_date = max(current_date, last_event.date_start + timedelta(days=1)) current_date = make_aware(datetime.combine(current_date, time.min)) added_events = [] for day in iter_daterange(current_date, date_stop): if not getattr(self, 'on_day%d' % day.weekday()): continue event = self.gen_future_event(day) event.full_clean() event.save(force_insert=True) added_events.append(event) return added_events
[docs]class Planning(AbstractPlanning): class Meta(AbstractPlanning.Meta): swappable = swapper.swappable_setting('resax', 'Planning')