<commit_before>from typing import List, Tuple

import reversion
from django.db import models
from django.urls import reverse
from django.utils.html import format_html

from agir.lib.models import TimeStampedModel
from agir.people.models import Person
from .commentaires import Commentaire
from .common import ModeleGestionMixin, NumeroManager
from ..actions import Todo, NiveauTodo, Transition, no_todos
from ..typologies import TypeProjet, NiveauAcces, TypeDepense, TypeDocument

__all__ = ("Projet", "Participation", "ProjetMilitant")


class ProjetManager(NumeroManager):
    def from_event(self, event, user):
        try:
            projet = self.get(event=event)
        except self.model.DoesNotExist:
            with reversion.create_revision():
                reversion.set_comment("Création à partir d'un événement utilisateur")
                reversion.set_user(user)
                titre = event.name if len(event.name) < 40 else f"{event.name[:39]}…"
                return self.create(
                    event=event,
                    etat=Projet.Etat.CREE_PLATEFORME,
                    titre=titre,
                    type=event.subtype.related_project_type or TypeProjet.ACTIONS,
                    description="Ce projet a été généré automatiquement à partir d'un événement créé par un "
                    "utilisateur d'Action Populaire.",
                    origine=Projet.Origin.UTILISATEUR,
                )
        else:
            if projet.type != event.subtype.related_project_type:
                with reversion.create_revision():
                    reversion.set_comment(
                        "Changement du type d'événement associé par l'utilisateur"
                    )
                    reversion.set_user(user)
                    projet.type = (
                        event.subtype.related_project_type or TypeProjet.ACTIONS
                    )
                    projet.save(update_fields=["type"])

            return projet


@reversion.register(follow=["participations"])
class Projet(ModeleGestionMixin, TimeStampedModel):

    """Le projet regroupe ensemble des dépenses liées entre elles

    Un projet peut correspondre à un événement, ou à un type de de dépense précis.
    (par exemple « organisation du meeting du 16 janvier » ou « paiement salaire mars »)
    """

    objects = ProjetManager()

    class Etat(models.TextChoices):
        CREE_PLATEFORME = "DFI", "Créé par un·e militant·e"
        REFUSE = "REF", "Refusé"
        EN_CONSTITUTION = "ECO", "En cours de constitution"
        FINALISE = "FIN", "Finalisé par le secrétariat"
        RENVOI = "REN", "Renvoyé par l'équipe financière"
        CLOTURE = "CLO", "Clôturé"

    TRANSITIONS = {
        Etat.CREE_PLATEFORME: [
            Transition(
                nom="Accepter le projet",
                vers=Etat.EN_CONSTITUTION,
                permissions=["gestion.gerer_projet"],
                class_name="success",
            ),
            Transition(
                nom="Refuser le projet",
                vers=Etat.REFUSE,
                permissions=["gestion.gerer_projet"],
                class_name="failure",
            ),
        ],
        Etat.EN_CONSTITUTION: [
            Transition(
                nom="Finaliser le projet",
                vers=Etat.FINALISE,
                condition=no_todos,
                permissions=["gestion.gerer_projet"],
                class_name="success",
            )
        ],
        Etat.FINALISE: [
            Transition(
                nom="Renvoyer le projet",
                vers=Etat.RENVOI,
                permissions=["gestion.controler_projet"],
                class_name="failure",
            ),
            Transition(
                nom="Cloturer le projet",
                vers=Etat.CLOTURE,
                condition=no_todos,
                permissions=["gestion.controler_projet"],
                class_name="success",
            ),
        ],
    }

    class Origin(models.TextChoices):
        ADMINISTRATION = "A", "Créé sur l'admin"
        UTILISATEUR = "U", "Créé par un·e militant·e sur Action Populaire"
        REUNION_PUBLIQUE = "R", "Réunion publique suite à demande"

    titre = models.CharField(verbose_name="Titre du projet", max_length=200)
    type = models.CharField(
        verbose_name="Type de projet", choices=TypeProjet.choices, max_length=10
    )
    origine = models.CharField(
        verbose_name="Origine du projet",
        max_length=1,
        choices=Origin.choices,
        editable=False,
        default=Origin.ADMINISTRATION,
    )

    etat = models.CharField(
        verbose_name="Statut",
        max_length=3,
        choices=Etat.choices,
        blank=False,
        default=Etat.CREE_PLATEFORME,
    )
    description = models.TextField(verbose_name="Description du projet", null=True)
    event = models.ForeignKey(
        to="events.Event",
        verbose_name="Événement sur la plateforme",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )

    niveau_acces = models.CharField(
        verbose_name="Niveau d'accès",
        max_length=1,
        choices=NiveauAcces.choices,
        blank=False,
        default=NiveauAcces.SANS_RESTRICTION,
    )

    documents = models.ManyToManyField(
        to="Document", related_name="projets", related_query_name="projet"
    )

    # Ce champ JSON est utilisé pour sauvegarder un certain nombre de réglages du projet.
    # (ci-dessous, une clé "a.b" désigne la clé "b" de l'objet à la clé "a" de l'objet détails)
    # Pour le moment, les clés suivantes sont définies :
    # - la clé "documents.absents" est une liste de type de documents normalement requis pour ce type de projets, mais
    #   indiqué comme explicitement absent sur le projet concerné.
    details = models.JSONField("Détails", default=dict)

    def todos(self):
        return todos(self)

    @property
    def transitions(self):
        return self.TRANSITIONS.get(self.etat, [])

    search_config = (
        ("numero", "B"),
        ("titre", "A"),
        ("description", "C"),
        ("event__name", "B"),
        ("event__description", "C"),
        ("event__location_name", "C"),
        ("event__location_city", "C"),
        ("event__contact_name", "D"),
        ("event__contact_phone", "D"),
    )<commit_msg>feat: afficher l'origine du projet dans les autocompletes<commit_after>from typing import List, Tuple

import reversion
from django.db import models
from django.urls import reverse
from django.utils.html import format_html

from agir.lib.models import TimeStampedModel
from agir.people.models import Person
from .commentaires import Commentaire
from .common import ModeleGestionMixin, NumeroManager
from ..actions import Todo, NiveauTodo, Transition, no_todos
from ..typologies import TypeProjet, NiveauAcces, TypeDepense, TypeDocument

__all__ = ("Projet", "Participation", "ProjetMilitant")


class ProjetManager(NumeroManager):
    def from_event(self, event, user):
        try:
            projet = self.get(event=event)
        except self.model.DoesNotExist:
            with reversion.create_revision():
                reversion.set_comment("Création à partir d'un événement utilisateur")
                reversion.set_user(user)
                titre = event.name if len(event.name) < 40 else f"{event.name[:39]}…"
                return self.create(
                    event=event,
                    etat=Projet.Etat.CREE_PLATEFORME,
                    titre=titre,
                    type=event.subtype.related_project_type or TypeProjet.ACTIONS,
                    description="Ce projet a été généré automatiquement à partir d'un événement créé par un "
                    "utilisateur d'Action Populaire.",
                    origine=Projet.Origin.UTILISATEUR,
                )
        else:
            if projet.type != event.subtype.related_project_type:
                with reversion.create_revision():
                    reversion.set_comment(
                        "Changement du type d'événement associé par l'utilisateur"
                    )
                    reversion.set_user(user)
                    projet.type = (
                        event.subtype.related_project_type or TypeProjet.ACTIONS
                    )
                    projet.save(update_fields=["type"])

            return projet


@reversion.register(follow=["participations"])
class Projet(ModeleGestionMixin, TimeStampedModel):

    """Le projet regroupe ensemble des dépenses liées entre elles

    Un projet peut correspondre à un événement, ou à un type de de dépense précis.
    (par exemple « organisation du meeting du 16 janvier » ou « paiement salaire mars »)
    """

    objects = ProjetManager()

    class Etat(models.TextChoices):
        CREE_PLATEFORME = "DFI", "Créé par un·e militant·e"
        REFUSE = "REF", "Refusé"
        EN_CONSTITUTION = "ECO", "En cours de constitution"
        FINALISE = "FIN", "Finalisé par le secrétariat"
        RENVOI = "REN", "Renvoyé par l'équipe financière"
        CLOTURE = "CLO", "Clôturé"

    TRANSITIONS = {
        Etat.CREE_PLATEFORME: [
            Transition(
                nom="Accepter le projet",
                vers=Etat.EN_CONSTITUTION,
                permissions=["gestion.gerer_projet"],
                class_name="success",
            ),
            Transition(
                nom="Refuser le projet",
                vers=Etat.REFUSE,
                permissions=["gestion.gerer_projet"],
                class_name="failure",
            ),
        ],
        Etat.EN_CONSTITUTION: [
            Transition(
                nom="Finaliser le projet",
                vers=Etat.FINALISE,
                condition=no_todos,
                permissions=["gestion.gerer_projet"],
                class_name="success",
            )
        ],
        Etat.FINALISE: [
            Transition(
                nom="Renvoyer le projet",
                vers=Etat.RENVOI,
                permissions=["gestion.controler_projet"],
                class_name="failure",
            ),
            Transition(
                nom="Cloturer le projet",
                vers=Etat.CLOTURE,
                condition=no_todos,
                permissions=["gestion.controler_projet"],
                class_name="success",
            ),
        ],
    }

    class Origin(models.TextChoices):
        ADMINISTRATION = "A", "Créé par les équipes de campagne"
        UTILISATEUR = "U", "Créé par un·e militant·e sur Action Populaire"
        REUNION_PUBLIQUE = "R", "Réunion publique suite à demande"

    SHORT_ORIGIN = {
        Origin.ADMINISTRATION: "campagne",
        Origin.UTILISATEUR: "militant",
        Origin.REUNION_PUBLIQUE: "réunion publique",
    }

    titre = models.CharField(verbose_name="Titre du projet", max_length=200)
    type = models.CharField(
        verbose_name="Type de projet", choices=TypeProjet.choices, max_length=10
    )
    origine = models.CharField(
        verbose_name="Origine du projet",
        max_length=1,
        choices=Origin.choices,
        editable=False,
        default=Origin.ADMINISTRATION,
    )

    etat = models.CharField(
        verbose_name="Statut",
        max_length=3,
        choices=Etat.choices,
        blank=False,
        default=Etat.CREE_PLATEFORME,
    )
    description = models.TextField(verbose_name="Description du projet", null=True)
    event = models.ForeignKey(
        to="events.Event",
        verbose_name="Événement sur la plateforme",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )

    niveau_acces = models.CharField(
        verbose_name="Niveau d'accès",
        max_length=1,
        choices=NiveauAcces.choices,
        blank=False,
        default=NiveauAcces.SANS_RESTRICTION,
    )

    documents = models.ManyToManyField(
        to="Document", related_name="projets", related_query_name="projet"
    )

    # Ce champ JSON est utilisé pour sauvegarder un certain nombre de réglages du projet.
    # (ci-dessous, une clé "a.b" désigne la clé "b" de l'objet à la clé "a" de l'objet détails)
    # Pour le moment, les clés suivantes sont définies :
    # - la clé "documents.absents" est une liste de type de documents normalement requis pour ce type de projets, mais
    #   indiqué comme explicitement absent sur le projet concerné.
    details = models.JSONField("Détails", default=dict)

    def todos(self):
        return todos(self)

    @property
    def transitions(self):
        # noinspection PyTypeChecker
        return self.TRANSITIONS.get(self.etat, [])

    def __str__(self):
        if len(self.titre) < 60:
            titre = self.titre
        else:
            titre = f"{self.titre[:60]}…"

        # noinspection PyTypeChecker
        return f"{titre} ({self.SHORT_ORIGIN[self.origine]})"

    search_config = (
        ("numero", "B"),
        ("titre", "A"),
        ("description", "C"),
        ("event__name", "B"),
        ("event__description", "C"),
        ("event__location_name", "C"),
        ("event__location_city", "C"),
        ("event__contact_name", "D"),
        ("event__contact_phone", "D"),
    )
{'fraction_non_alphanumeric': 0.09742076998714166, 'fraction_numerical': 0.0024203918009227743, 'mean_word_length': 2.0842080709120596, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6175996', 'n_tokens_mistral': 4065, 'n_tokens_neox': 3935, 'n_words': 879}