Esempio n. 1
0
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,
        )
Esempio n. 5
0
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")
Esempio n. 7
0
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
Esempio n. 8
0
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])
Esempio n. 9
0
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", )