class Announcement(models.Model): objects = AnnouncementQuerySet.as_manager() icon = models.CharField( verbose_name="icône", max_length=200, default="exclamation", help_text=format_html( 'Indiquez le nom d\'une icône dans <a href="{icon_link}">cette liste</a>', icon_link="https://fontawesome.com/v4.7.0/icons/", ), ) content = DescriptionField( verbose_name=_("Contenu de la notification"), allowed_tags=["p", "div", "strong", "em", "a", "br"], ) link = models.URLField(verbose_name=_("Lien"), blank=True) start_date = models.DateTimeField(verbose_name=_("Date de début"), default=timezone.now) end_date = models.DateTimeField(verbose_name=_("Date de fin"), null=True, blank=True) segment = models.ForeignKey( to="mailing.Segment", on_delete=models.CASCADE, related_name="notifications", related_query_name="notification", null=True, blank=True, help_text= _("Segment des personnes auquel ce message sera montré (laisser vide pour montrer à tout le monde)" ), ) def __str__(self): no_tag_content = sanitize_html(self.content, tags=[]) if len(no_tag_content) > 100: no_tag_content = no_tag_content[:97] + "..." return f"« {no_tag_content} »" class Meta: verbose_name = _("Annonce") indexes = (models.Index(fields=("start_date", "end_date"), name="notification_query_index"), ) ordering = ("start_date", "end_date")
class Poll(BaseAPIResource): title = models.CharField(_("Titre de la consultation"), max_length=255) description = DescriptionField( _("Description de la consultation"), allowed_tags=settings.ADMIN_ALLOWED_TAGS, help_text=_("Le texte de description affiché pour tous les insoumis"), ) start = models.DateTimeField( _("Date et heure de début de la consultation"), help_text=_( "La consultation sera automatiquement ouverte à ce moment"), ) end = models.DateTimeField( _("Date et heure de fin de la consultation"), help_text=_("La consultation sera automatiquement fermée à ce moment"), ) rules = JSONField( _("Les règles du vote"), encoder=DjangoJSONEncoder, help_text=_( "Un object JSON décrivant les règles. Actuellement, sont reconnues `options`," "`min_options`, `max_options` et `verified_user`"), default=dict, ) tags = models.ManyToManyField( "people.PersonTag", related_name="polls", related_query_name="poll", blank=True, verbose_name="Tag à ajouter aux participant⋅es", ) confirmation_note = DescriptionField( "Note après participation", allowed_tags=settings.ADMIN_ALLOWED_TAGS, help_text= _("Note montrée à l'utilisateur une fois la participation enregistrée." ), blank=True, ) authorized_segment = models.ForeignKey( "mailing.Segment", on_delete=models.SET_NULL, related_name="+", related_query_name="+", blank=True, null=True, verbose_name="Limiter l'accès à la consultation à ce segment", ) def make_choice(self, person, options): with transaction.atomic(): if self.tags.all().count() > 0: person.tags.add(*self.tags.all()) PollChoice.objects.create( person=person, poll=self, selection=[option.pk for option in options]) def html_description(self): return mark_safe(markdown.markdown(self.description)) def __str__(self): return self.title class Meta: ordering = ("-start", )
class EventSubtype(BaseSubtype): TYPE_GROUP_MEETING = "G" TYPE_PUBLIC_MEETING = "M" TYPE_PUBLIC_ACTION = "A" TYPE_OTHER_EVENTS = "O" TYPE_CHOICES = ( (TYPE_GROUP_MEETING, _("Réunion privée de groupe")), (TYPE_PUBLIC_MEETING, _("Événement public")), (TYPE_PUBLIC_ACTION, _("Action publique")), (TYPE_OTHER_EVENTS, _("Autre")), ) TYPES_PARAMETERS = { TYPE_GROUP_MEETING: { "color": "#4a64ac", "icon_name": "comments" }, TYPE_PUBLIC_MEETING: { "color": "#e14b35", "icon_name": "bullhorn" }, TYPE_PUBLIC_ACTION: { "color": "#c2306c", "icon_name": "exclamation" }, TYPE_OTHER_EVENTS: { "color": "#49b37d", "icon_name": "calendar" }, } TYPE_DESCRIPTION = { TYPE_GROUP_MEETING: _("Une réunion qui concerne principalement les membres du groupes, et non le public de" " façon générale. Par exemple, la réunion hebdomadaire du groupe, une réunion de travail," " ou l'audition d'une association"), TYPE_PUBLIC_MEETING: _("Un événement ouvert à tous les publics, au-delà des membres du groupe, mais" " qui aura lieu dans un lieu privé. Par exemple, un événement public avec un orateur," " une projection ou un concert."), TYPE_PUBLIC_ACTION: _("Une action qui se déroulera dans un lieu public et qui aura comme objectif principal" " d'aller à la rencontre ou d'atteindre des personnes extérieures à la FI" ), TYPE_OTHER_EVENTS: _("Tout autre type d'événement qui ne rentre pas dans les catégories ci-dessus." ), } EVENT_SUBTYPE_REQUIRED_DOCUMENT_TYPE_CHOICES = [ choice for choice in TypeDocument.choices if f"{TypeDocument.ATTESTATION}-" in choice[0] ] type = models.CharField(_("Type d'événement"), max_length=1, choices=TYPE_CHOICES) default_description = DescriptionField( verbose_name=_("description par défaut"), blank=True, help_text= "La description par défaut pour les événements de ce sous-type.", allowed_tags=settings.ADMIN_ALLOWED_TAGS, ) default_image = StdImageField( _("image par défaut"), upload_to=banner_path, variations=settings.BANNER_CONFIG, blank=True, help_text=_( "L'image associée par défaut à un événement de ce sous-type."), ) has_priority = models.BooleanField( "Le sous-type d'événement est prioritaire", default=False, help_text= "Le sous-type d'événement apparaîtra en premier dans la liste des sous-types disponibles, " "par exemple lors de la création d'un événement.", ) related_project_type = models.CharField( verbose_name="Type de projet de gestion associé", choices=TypeProjet.choices, max_length=10, blank=True, default="", ) required_documents = ArrayField( verbose_name="Attestations requises", base_field=models.CharField( choices=EVENT_SUBTYPE_REQUIRED_DOCUMENT_TYPE_CHOICES, max_length=10, ), null=False, blank=False, default=list, ) report_person_form = models.ForeignKey( "people.PersonForm", verbose_name="Formulaire de bilan", help_text= "Les organisateur·ices des événements de ce type seront invité·es à remplir ce formulaire une fois " "l'événement terminé", related_name="event_subtype", related_query_name="event_subtype", null=True, blank=True, on_delete=models.PROTECT, ) is_editable = models.BooleanField( "Les événements de ce sous-types seront modifiables", default=True, help_text= "Les événements de ce sous-type pourront être modifiés par les organisateur·ices, " "et non seulement par les administrateur·ices", ) class Meta: verbose_name = _("Sous-type d'événement") verbose_name_plural = _("Sous-types d'événement") ordering = ["-has_priority"] def __str__(self): return self.description
class Event( ExportModelOperationsMixin("event"), BaseAPIResource, LocationMixin, ImageMixin, DescriptionMixin, ContactMixin, ): """ Model that represents an event """ objects = EventManager() name = models.CharField(_("nom"), max_length=255, blank=False, help_text=_("Le nom de l'événement")) VISIBILITY_ADMIN = "A" VISIBILITY_ORGANIZER = "O" VISIBILITY_PUBLIC = "P" VISIBILITY_CHOICES = ( (VISIBILITY_ADMIN, "Caché"), (VISIBILITY_ORGANIZER, "Visible par les organisateurs"), (VISIBILITY_PUBLIC, "Public"), ) visibility = models.CharField( "Visibilité", max_length=1, choices=VISIBILITY_CHOICES, default=VISIBILITY_PUBLIC, ) subtype = models.ForeignKey( "EventSubtype", verbose_name="Sous-type", related_name="events", on_delete=models.PROTECT, default=get_default_subtype, ) tags = models.ManyToManyField("EventTag", related_name="events", blank=True) start_time = CustomDateTimeField(_("date et heure de début"), blank=False) end_time = CustomDateTimeField(_("date et heure de fin"), blank=False) timezone = models.CharField( "Fuseau horaire", max_length=255, choices=((name, name) for name in pytz.all_timezones), default=timezone.get_default_timezone().zone, blank=False, null=False, ) max_participants = models.IntegerField("Nombre maximum de participants", blank=True, null=True) allow_guests = models.BooleanField( "Autoriser les participant⋅e⋅s à inscrire des invité⋅e⋅s", default=False) facebook = FacebookEventField("Événement correspondant sur Facebook", blank=True) attendees = models.ManyToManyField("people.Person", related_name="events", through="RSVP") organizers = models.ManyToManyField("people.Person", related_name="organized_events", through="OrganizerConfig") organizers_groups = models.ManyToManyField( "groups.SupportGroup", related_name="organized_events", through="OrganizerConfig", ) report_image = StdImageField( verbose_name=_("image de couverture"), blank=True, variations={ "thumbnail": (400, 250), "banner": (1200, 400) }, upload_to=report_image_path, help_text=_( "Cette image apparaîtra en tête de votre compte-rendu, et dans les partages que vous ferez du" " compte-rendu sur les réseaux sociaux."), ) report_content = DescriptionField( verbose_name=_("compte-rendu de l'événement"), blank=True, allowed_tags="allowed_tags", help_text= _("Ajoutez un compte-rendu de votre événement. N'hésitez pas à inclure des photos." ), ) report_summary_sent = models.BooleanField( "Le mail de compte-rendu a été envoyé", default=False) subscription_form = models.OneToOneField("people.PersonForm", null=True, blank=True, on_delete=models.PROTECT) payment_parameters = JSONField( verbose_name=_("Paramètres de paiement"), null=True, blank=True, help_text=EVENT_PAYMENT_PARAMETERS_DOCUMENTATION, ) scanner_event = models.IntegerField( "L'ID de l'événement sur le logiciel de tickets", blank=True, null=True) scanner_category = models.IntegerField( "La catégorie que doivent avoir les tickets sur scanner", blank=True, null=True) enable_jitsi = models.BooleanField("Activer la visio-conférence", default=False) participation_template = models.TextField( _("Template pour la page de participation"), blank=True, null=True) do_not_list = models.BooleanField( "Ne pas lister l'événement", default=False, help_text= "L'événement n'apparaîtra pas sur la carte, ni sur le calendrier " "et ne sera pas cherchable via la recherche interne ou les moteurs de recherche.", ) meta = JSONField( _("Informations supplémentaires."), default=dict, blank=True, encoder=CustomJSONEncoder, ) FOR_USERS_ALL = "A" FOR_USERS_INSOUMIS = "I" FOR_USERS_2022 = "2" FOR_USERS_CHOICES = ( (FOR_USERS_ALL, "Tous les utilisateurs"), (FOR_USERS_INSOUMIS, "Les insoumis⋅es"), (FOR_USERS_2022, "Les signataires « Nous Sommes Pour ! »"), ) for_users = models.CharField( "Utilisateur⋅ices de la plateforme concerné⋅es par l'événement", default=FOR_USERS_ALL, max_length=1, blank=False, choices=FOR_USERS_CHOICES, ) online_url = models.URLField( "Url de visio-conférence", default="", blank=True, ) suggestion_segment = models.ForeignKey( to="mailing.Segment", verbose_name="Segment de suggestion", on_delete=models.SET_NULL, related_name="suggested_events", related_query_name="suggested_event", null=True, blank=True, help_text=("Segment des personnes auquel cet événement sera suggéré."), ) class Meta: verbose_name = _("événement") verbose_name_plural = _("événements") ordering = ("-start_time", "-end_time") permissions = ( # DEPRECIATED: every_event was set up as a potential solution to Rest Framework django permissions # Permission class default behaviour of requiring both global permissions and object permissions before # allowing users. Was not used in the end.s ("every_event", _("Peut éditer tous les événements")), ("view_hidden_event", _("Peut voir les événements non publiés")), ) indexes = ( models.Index(fields=["start_time", "end_time"], name="events_datetime_index"), models.Index(fields=["end_time"], name="events_end_time_index"), models.Index(fields=["start_time", "end_time", "id"], name="events_datetime_id_index"), ) def __str__(self): return f"{self.name} ({self.get_display_date()})" def __repr__(self): return f"{self.__class__.__name__}(id={str(self.pk)!r}, name={self.name!r})" def to_ics(self, text_only_description=False): event_url = front_url("view_event", args=[self.pk], auto_login=False) organizer = Organizer(email=self.contact_email, common_name=self.contact_name) if text_only_description: description = textify(self.description) + " " + event_url else: description = self.description + f"<p>{event_url}</p>" return ics.Event( name=self.name, begin=self.start_time, end=self.end_time, uid=str(self.pk), description=description, location=self.short_address, url=event_url, categories=[self.subtype.get_type_display()], geo=self.coordinates, organizer=organizer, ) def _get_participants_counts(self): self.all_attendee_count, self.confirmed_attendee_count = ( self.__class__.objects.with_participants().values_list( "all_attendee_count", "confirmed_attendee_count").get(id=self.id)) @property def participants(self): if not hasattr(self, "all_attendee_count"): self._get_participants_counts() return self.all_attendee_count @property def participants_confirmes(self): if not hasattr(self, "confirmed_attendee_count"): self._get_participants_counts() return self.confirmed_attendee_count @property def type(self): return self.subtype.type @property def local_start_time(self): tz = pytz.timezone(self.timezone) return self.start_time.astimezone(tz) @property def local_end_time(self): tz = pytz.timezone(self.timezone) return self.end_time.astimezone(tz) def get_display_date(self): start_time = self.local_start_time end_time = self.local_end_time if start_time.date() == end_time.date(): date = formats.date_format(start_time, "DATE_FORMAT") return _("le {date}, de {start_hour} à {end_hour} ({tz})").format( date=date, start_hour=formats.time_format(start_time, "TIME_FORMAT"), end_hour=formats.time_format(end_time, "TIME_FORMAT"), tz=self.timezone, ) return _( "du {start_date}, {start_time} au {end_date}, {end_time} ({tz})" ).format( start_date=formats.date_format(start_time, "DATE_FORMAT"), start_time=formats.date_format(start_time, "TIME_FORMAT"), end_date=formats.date_format(end_time, "DATE_FORMAT"), end_time=formats.date_format(end_time, "TIME_FORMAT"), tz=self.timezone, ) def get_simple_display_date(self): return _("le {date} à {time} ({tz})").format( date=formats.date_format(self.local_start_time, "DATE_FORMAT"), time=formats.time_format(self.local_start_time, "TIME_FORMAT"), tz=self.timezone, ) def is_past(self): return timezone.now() > self.end_time def is_current(self): return self.start_time < timezone.now() < self.end_time def clean(self): if self.start_time and self.end_time and self.end_time < self.start_time: raise ValidationError({ "end_time": _("La date de fin de l'événement doit être postérieure à sa date de début." ) }) def get_price_display(self): if self.payment_parameters is None: return None base_price = self.payment_parameters.get("price", 0) min_price = base_price max_price = base_price for mapping in self.payment_parameters.get("mappings", []): prices = [m["price"] for m in mapping["mapping"]] min_price += min(prices) max_price += max(prices) if min_price == max_price == 0: if "free_pricing" in self.payment_parameters: return "Prix libre" else: return None if min_price == max_price: display = "{} €".format(floatformat(min_price / 100, 2)) else: display = "de {} à {} €".format(floatformat(min_price / 100, 2), floatformat(max_price / 100, 2)) if "free_pricing" in self.payment_parameters: display += " + montant libre" return display @property def is_free(self): return self.payment_parameters is None @property def is_2022(self): return self.for_users == self.FOR_USERS_2022 def get_price(self, submission_data: dict = None): price = self.payment_parameters.get("price", 0) if submission_data is None: return price for mapping in self.payment_parameters.get("mappings", []): values = [ submission_data.get(field) for field in mapping["fields"] ] d = { tuple(v for v in m["values"]): m["price"] for m in mapping["mapping"] } price += d[tuple(values)] if "free_pricing" in self.payment_parameters: field = self.payment_parameters["free_pricing"] price += max(0, int(submission_data.get(field, 0) * 100)) return price def get_absolute_url(self): return front_url("view_event", args=[self.pk]) def get_google_calendar_url(self): # https://github.com/InteractionDesignFoundation/add-event-to-calendar-docs/blob/master/services/google.md df = "%Y%m%dT%H%M00" start_time = self.local_start_time.strftime(df) end_time = self.local_end_time.strftime(df) details = f"{self.description}<p><a href={self.get_absolute_url()}>Page de l'événement</a></p>" if self.online_url: details += f"<p><a href={self.online_url}>Rejoindre en ligne</a></p>" query = { "action": "TEMPLATE", "ctz": self.timezone, "text": self.name, "dates": f"{start_time}/{end_time}", "location": self.short_address, "details": details, "sprop": f"website:{self.get_absolute_url()}", } return f"https://calendar.google.com/calendar/render?{urlencode(query)}&sprop=name:Action%20Populaire" def can_rsvp(self, person): return True def get_meta_image(self): if hasattr(self, "image") and self.image: return urljoin(settings.FRONT_DOMAIN, self.image.url) # Use content hash as cache key for the auto-generated meta image content = ":".join(( self.name, self.location_zip, self.location_city, str(self.coordinates), str(self.start_time), )) content_hash = hashlib.sha1(content.encode("utf-8")).hexdigest()[:8] return front_url( "view_og_image_event", kwargs={ "pk": self.pk, "cache_key": content_hash }, absolute=True, )
class Announcement(models.Model): objects = AnnouncementQuerySet.as_manager() title = models.CharField( verbose_name="Titre de l'annonce", max_length=200, help_text= "Ce texte sera utilisé comme titre et texte du lien de l'annonce", blank=False, ) link = models.URLField(verbose_name="Lien", blank=False) content = DescriptionField(verbose_name="Contenu", blank=False) image = StdImageField( verbose_name="Bannière", validators=[MinSizeValidator(255, 160)], variations={ "desktop": { "width": 255, "height": 130, "crop": True }, "mobile": { "width": 160, "height": 160, "crop": True }, }, upload_to=dynamic_filenames.FilePattern( filename_pattern= "activity/announcements/{uuid:.2base32}/{uuid:s}{ext}"), null=True, blank=True, ) start_date = models.DateTimeField(verbose_name="Date de début", default=timezone.now) end_date = models.DateTimeField(verbose_name="Date de fin", null=True, blank=True) segment = models.ForeignKey( to="mailing.Segment", on_delete=models.CASCADE, related_name="notifications", related_query_name="notification", null=True, blank=True, help_text= ("Segment des personnes auquel ce message sera montré (laisser vide pour montrer à tout le monde)" ), ) priority = models.IntegerField( verbose_name="Priorité", default=0, help_text= "Permet de modifier l'ordre d'affichage des annonces. Les valeurs plus élevées sont affichées avant." " Deux annonces de même priorité sont affichées dans l'ordre anti-chronologique (par date de début)", ) def __str__(self): return f"« {self.title} »" class Meta: verbose_name = "Annonce" indexes = (models.Index( fields=("-start_date", "end_date"), name="announcement_date_index", ), ) ordering = ("-start_date", "end_date")
class PersonForm(TimeStampedModel): objects = PersonFormQueryset.as_manager() title = models.CharField(_("Titre"), max_length=250) slug = models.SlugField(_("Slug"), max_length=50, unique=True) published = models.BooleanField(_("Publié"), default=True) result_url_uuid = models.UUIDField( "UUID pour l'affichage des résultats", editable=False, null=True ) start_time = models.DateTimeField( _("Date d'ouverture du formulaire"), null=True, blank=True ) end_time = models.DateTimeField( _("Date de fermeture du formulaire"), null=True, blank=True ) editable = models.BooleanField( _("Les répondant⋅e⋅s peuvent modifier leurs réponses"), default=False ) allow_anonymous = models.BooleanField( _("Les répondant⋅es n'ont pas besoin d'être connecté⋅es"), default=False ) send_answers_to = models.EmailField( _("Envoyer les réponses par email à une adresse email (facultatif)"), blank=True ) description = DescriptionField( _("Description"), allowed_tags=settings.ADMIN_ALLOWED_TAGS, help_text=_( "Description visible en haut de la page de remplissage du formulaire" ), ) send_confirmation = models.BooleanField( _("Envoyer une confirmation par email"), default=False ) confirmation_note = DescriptionField( _("Note après complétion"), allowed_tags=settings.ADMIN_ALLOWED_TAGS, help_text=_( "Note montrée (et éventuellement envoyée par email) à l'utilisateur une fois le formulaire validé." ), ) before_message = DescriptionField( _("Note avant ouverture"), allowed_tags=settings.ADMIN_ALLOWED_TAGS, help_text=_( "Note montrée à l'utilisateur qui essaye d'accéder au formulaire avant son ouverture." ), blank=True, ) after_message = DescriptionField( _("Note de fermeture"), allowed_tags=settings.ADMIN_ALLOWED_TAGS, help_text=_( "Note montrée à l'utilisateur qui essaye d'accéder au formulaire après sa date de fermeture." ), blank=True, ) required_tags = models.ManyToManyField( "PersonTag", related_name="authorized_forms", related_query_name="authorized_form", blank=True, ) segment = models.ForeignKey( "mailing.Segment", on_delete=models.SET_NULL, related_name="person_forms", related_query_name="person_form", blank=True, null=True, ) unauthorized_message = DescriptionField( _("Note pour les personnes non autorisées"), allowed_tags=settings.ADMIN_ALLOWED_TAGS, help_text=_( "Note montrée à tout utilisateur qui n'aurait pas le tag nécessaire pour afficher le formulaire." ), blank=True, ) main_question = models.CharField( _("Intitulé de la question principale"), max_length=200, help_text=_("Uniquement utilisée si des choix de tags sont demandés."), blank=True, ) tags = models.ManyToManyField( "PersonTag", related_name="forms", related_query_name="form", blank=True ) custom_fields = JSONField(_("Champs"), blank=False, default=default_custom_forms) config = JSONField(_("Configuration"), blank=True, default=dict) campaign_template = models.ForeignKey( "nuntius.Campaign", verbose_name="Créer une campagne à partir de ce modèle", blank=True, null=True, on_delete=models.PROTECT, ) @property def submit_label(self): return self.config.get("submit_label", "Envoyer") @property def fields_dict(self): return OrderedDict( (field["id"], field) for field in chain( ( field for fieldset in self.custom_fields for field in fieldset.get("fields", []) ), self.config.get("hidden_fields", []), ) ) @property def is_open(self): now = timezone.now() return (self.start_time is None or self.start_time < now) and ( self.end_time is None or now < self.end_time ) def is_authorized(self, person): return ( not self.required_tags.all() or (person.tags.all() & self.required_tags.all()).exists() ) and ( self.segment is None or self.segment.get_subscribers_queryset().filter(id=person.id).exists() ) @property def html_closed_message(self): now = timezone.now() if self.start_time is not None and self.start_time > now: if self.before_message: return self.html_before_message() else: return "Ce formulaire n'est pas encore ouvert." else: if self.after_message: return self.html_after_message() else: return "Ce formulaire est maintenant fermé." def __str__(self): return "« {} »".format(self.title) class Meta: verbose_name = _("Formulaire")
class EventSubtype(BaseSubtype): TYPE_GROUP_MEETING = "G" TYPE_PUBLIC_MEETING = "M" TYPE_PUBLIC_ACTION = "A" TYPE_OTHER_EVENTS = "O" TYPE_CHOICES = ( (TYPE_GROUP_MEETING, _("Réunion de groupe")), (TYPE_PUBLIC_MEETING, _("Réunion publique")), (TYPE_PUBLIC_ACTION, _("Action publique")), (TYPE_OTHER_EVENTS, _("Autres type d'événements")), ) TYPES_PARAMETERS = { TYPE_GROUP_MEETING: {"color": "#4a64ac", "icon_name": "comments"}, TYPE_PUBLIC_MEETING: {"color": "#e14b35", "icon_name": "bullhorn"}, TYPE_PUBLIC_ACTION: {"color": "#c2306c", "icon_name": "exclamation"}, TYPE_OTHER_EVENTS: {"color": "#49b37d", "icon_name": "calendar"}, } TYPE_DESCRIPTION = { TYPE_GROUP_MEETING: _( "Une réunion qui concerne principalement les membres du groupes, et non le public de" " façon générale. Par exemple, la réunion hebdomadaire du groupe, une réunion de travail," " ou l'audition d'une association" ), TYPE_PUBLIC_MEETING: _( "Une réunion ouverts à tous les publics, au-delà des membres du groupe d'action, mais" " qui aura lieu dans un lieu privé. Par exemple, une réunion publique avec un orateur," " une projection ou un concert." ), TYPE_PUBLIC_ACTION: _( "Une action qui se déroulera dans un lieu public et qui aura comme objectif principal" " d'aller à la rencontre ou d'atteindre des personnes extérieures à la FI" ), TYPE_OTHER_EVENTS: _( "Tout autre type d'événement qui ne rentre pas dans les catégories ci-dessus." ), } type = models.CharField(_("Type d'événement"), max_length=1, choices=TYPE_CHOICES) default_description = DescriptionField( verbose_name=_("description par défaut"), blank=True, help_text="La description par défaut pour les événements de ce sous-type.", allowed_tags=settings.ADMIN_ALLOWED_TAGS, ) default_image = StdImageField( _("image par défaut"), upload_to=banner_path, variations=settings.BANNER_CONFIG, blank=True, help_text=_("L'image associée par défaut à un événement de ce sous-type."), ) class Meta: verbose_name = _("Sous-type d'événement") verbose_name_plural = _("Sous-types d'événement") def __str__(self): return self.description
class Event( ExportModelOperationsMixin("event"), BaseAPIResource, NationBuilderResource, LocationMixin, ImageMixin, DescriptionMixin, ContactMixin, ): """ Model that represents an event """ objects = EventQuerySet.as_manager() name = models.CharField( _("nom"), max_length=255, blank=False, help_text=_("Le nom de l'événement") ) VISIBILITY_ADMIN = "A" VISIBILITY_ORGANIZER = "O" VISIBILITY_PUBLIC = "P" VISIBILITY_CHOICES = ( (VISIBILITY_ADMIN, "Caché"), (VISIBILITY_ORGANIZER, "Visible par les organisateurs"), (VISIBILITY_PUBLIC, "Public"), ) visibility = models.CharField( "Visibilité", max_length=1, choices=VISIBILITY_CHOICES, default=VISIBILITY_PUBLIC, ) subtype = models.ForeignKey( "EventSubtype", verbose_name="Sous-type", related_name="events", on_delete=models.PROTECT, default=get_default_subtype, ) nb_path = models.CharField(_("NationBuilder path"), max_length=255, blank=True) tags = models.ManyToManyField("EventTag", related_name="events", blank=True) start_time = CustomDateTimeField(_("date et heure de début"), blank=False) end_time = CustomDateTimeField(_("date et heure de fin"), blank=False) max_participants = models.IntegerField( "Nombre maximum de participants", blank=True, null=True ) allow_guests = models.BooleanField( "Autoriser les participant⋅e⋅s à inscrire des invité⋅e⋅s", default=False ) facebook = FacebookEventField("Événement correspondant sur Facebook", blank=True) attendees = models.ManyToManyField( "people.Person", related_name="events", through="RSVP" ) organizers = models.ManyToManyField( "people.Person", related_name="organized_events", through="OrganizerConfig" ) organizers_groups = models.ManyToManyField( "groups.SupportGroup", related_name="organized_events", through="OrganizerConfig", ) report_image = StdImageField( verbose_name=_("image de couverture"), blank=True, variations={"thumbnail": (400, 250), "banner": (1200, 400)}, upload_to=report_image_path, help_text=_( "Cette image apparaîtra en tête de votre compte-rendu, et dans les partages que vous ferez du" " compte-rendu sur les réseaux sociaux." ), ) report_content = DescriptionField( verbose_name=_("compte-rendu de l'événement"), blank=True, allowed_tags="allowed_tags", help_text=_( "Ajoutez un compte-rendu de votre événement. N'hésitez pas à inclure des photos." ), ) report_summary_sent = models.BooleanField( "Le mail de compte-rendu a été envoyé", default=False ) subscription_form = models.OneToOneField( "people.PersonForm", null=True, blank=True, on_delete=models.PROTECT ) payment_parameters = JSONField( verbose_name=_("Paramètres de paiement"), null=True, blank=True ) scanner_event = models.IntegerField( "L'ID de l'événement sur le logiciel de tickets", blank=True, null=True ) scanner_category = models.IntegerField( "La catégorie que doivent avoir les tickets sur scanner", blank=True, null=True ) enable_jitsi = models.BooleanField("Activer la visio-conférence", default=False) participation_template = models.TextField( _("Template pour la page de participation"), blank=True, null=True ) do_not_list = models.BooleanField( "Ne pas lister l'événement", default=False, help_text="L'événement n'apparaîtra pas sur la carte, ni sur le calendrier " "et ne sera pas cherchable via la recherche interne ou les moteurs de recherche.", ) legal = JSONField( _("Informations juridiques"), default=dict, blank=True, encoder=CustomJSONEncoder, ) class Meta: verbose_name = _("événement") verbose_name_plural = _("événements") ordering = ("-start_time", "-end_time") permissions = ( # DEPRECIATED: every_event was set up as a potential solution to Rest Framework django permissions # Permission class default behaviour of requiring both global permissions and object permissions before # allowing users. Was not used in the end.s ("every_event", _("Peut éditer tous les événements")), ("view_hidden_event", _("Peut voir les événements non publiés")), ) indexes = ( models.Index( fields=["start_time", "end_time"], name="events_datetime_index" ), models.Index(fields=["end_time"], name="events_end_time_index"), models.Index(fields=["nb_path"], name="events_nb_path_index"), ) def __str__(self): return f"{self.name} ({self.get_display_date()})" def __repr__(self): return f"{self.__class__.__name__}(id={str(self.pk)!r}, name={self.name!r})" def to_ics(self): event_url = front_url("view_event", args=[self.pk], auto_login=False) return ics.Event( name=self.name, begin=self.start_time, end=self.end_time, uid=str(self.pk), description=self.description + f"<p>{event_url}</p>", location=self.short_address, url=event_url, ) @property def participants(self): try: return self.all_attendee_count except AttributeError: if self.subscription_form: return ( self.rsvps.annotate( identified_guests_count=Count("identified_guests") ).aggregate(participants=Sum(F("identified_guests_count") + 1))[ "participants" ] or 0 ) return ( self.rsvps.aggregate(participants=Sum(models.F("guests") + 1))[ "participants" ] or 0 ) @property def type(self): return self.subtype.type def get_display_date(self): tz = timezone.get_current_timezone() start_time = self.start_time.astimezone(tz) end_time = self.end_time.astimezone(tz) if start_time.date() == end_time.date(): date = formats.date_format(start_time, "DATE_FORMAT") return _("le {date}, de {start_hour} à {end_hour}").format( date=date, start_hour=formats.time_format(start_time, "TIME_FORMAT"), end_hour=formats.time_format(end_time, "TIME_FORMAT"), ) return _("du {start_date}, {start_time} au {end_date}, {end_time}").format( start_date=formats.date_format(start_time, "DATE_FORMAT"), start_time=formats.date_format(start_time, "TIME_FORMAT"), end_date=formats.date_format(end_time, "DATE_FORMAT"), end_time=formats.date_format(end_time, "TIME_FORMAT"), ) def get_simple_display_date(self): tz = timezone.get_current_timezone() start_time = self.start_time.astimezone(tz) return _("le {date} à {time}").format( date=formats.date_format(start_time, "DATE_FORMAT"), time=formats.time_format(start_time, "TIME_FORMAT"), ) def is_past(self): return timezone.now() > self.end_time def is_current(self): return self.start_time < timezone.now() < self.end_time def clean(self): if self.start_time and self.end_time and self.end_time < self.start_time: raise ValidationError( { "end_time": _( "La date de fin de l'événement doit être postérieure à sa date de début." ) } ) def get_price_display(self): if self.payment_parameters is None: return None base_price = self.payment_parameters.get("price", 0) min_price = base_price max_price = base_price for mapping in self.payment_parameters.get("mappings", []): prices = [m["price"] for m in mapping["mapping"]] min_price += min(prices) max_price += max(prices) if min_price == max_price == 0: if "free_pricing" in self.payment_parameters: return "Prix libre" else: return None if min_price == max_price: display = "{} €".format(floatformat(min_price / 100, 2)) else: display = "de {} à {} €".format( floatformat(min_price / 100, 2), floatformat(max_price / 100, 2) ) if "free_pricing" in self.payment_parameters: display += " + montant libre" return display @property def is_free(self): return self.payment_parameters is None def get_price(self, submission_data: dict = None): price = self.payment_parameters.get("price", 0) if submission_data is None: return price for mapping in self.payment_parameters.get("mappings", []): values = [submission_data.get(field) for field in mapping["fields"]] d = {tuple(v for v in m["values"]): m["price"] for m in mapping["mapping"]} price += d[tuple(values)] if "free_pricing" in self.payment_parameters: field = self.payment_parameters["free_pricing"] price += max(0, int(submission_data.get(field, 0) * 100)) return price def get_absolute_url(self): return front_url("view_event", args=[self.pk])
class Notification(TimeStampedModel): STATUS_UNSEEN = "U" STATUS_SEEN = "S" STATUS_CLICKED = "C" STATUS_CHOICES = ( (STATUS_UNSEEN, "Non vue"), (STATUS_SEEN, "Vue"), (STATUS_CLICKED, "Cliquée"), ) person = models.ForeignKey( "people.Person", on_delete=models.CASCADE, related_name="notifications", related_query_name="notification", ) announcement = models.ForeignKey( Announcement, on_delete=models.CASCADE, related_name="notifications", related_query_name="notification", null=True, ) status = models.CharField("Status", max_length=1, choices=STATUS_CHOICES, default=STATUS_UNSEEN) icon = models.CharField( verbose_name="icône", max_length=200, help_text=format_html( 'Indiquez le nom d\'une icône dans <a href="{icon_link}">cette liste</a>', icon_link="https://fontawesome.com/v4.7.0/icons/", ), blank=True, ) content = DescriptionField( verbose_name=_("Contenu de la notification"), allowed_tags=["p", "div", "strong", "em", "a", "br"], blank=True, ) link = models.URLField(verbose_name=_("Lien"), blank=True) def get_absolute_url(self): return reverse("follow_notification", kwargs={"pk": self.id}) class Meta: indexes = [ models.Index( fields=["link"], name="internal_links", condition=Q(link__startswith=settings.FRONT_DOMAIN), ) ] unique_together = ("announcement", "person") constraints = [ models.CheckConstraint( check=(models.Q(content__isnull=False) | models.Q(announcement__isnull=False)) & (models.Q(icon__isnull=False) | models.Q(announcement__isnull=False)), name="has_content", ) ] ordering = ("-created", )