WIP Séparation des models : problème de référence circulaire
This commit is contained in:
		
							
								
								
									
										13
									
								
								src/agenda_culturel/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/agenda_culturel/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
from .utils import *
 | 
			
		||||
from .configuration import *
 | 
			
		||||
from .user import *
 | 
			
		||||
from .static_content import *
 | 
			
		||||
from .tag import *
 | 
			
		||||
from .place import *
 | 
			
		||||
from .organisation import *
 | 
			
		||||
from .event import *
 | 
			
		||||
from .message import *
 | 
			
		||||
from .import_recurrent import *
 | 
			
		||||
from .import_batch import *
 | 
			
		||||
from .category import *
 | 
			
		||||
from .special_period import *
 | 
			
		||||
							
								
								
									
										250
									
								
								src/agenda_culturel/models/category.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								src/agenda_culturel/models/category.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,250 @@
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
 | 
			
		||||
from autoslug import AutoSlugField
 | 
			
		||||
from colorfield.fields import ColorField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from ..models import Place, remove_accents
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Category(models.Model):
 | 
			
		||||
    default_name = "Sans catégorie"
 | 
			
		||||
    default_css_class = "cat-nocat"
 | 
			
		||||
    default_color = "#aaaaaa"
 | 
			
		||||
 | 
			
		||||
    COLOR_PALETTE = [
 | 
			
		||||
        ("#ea5545", "color 1"),
 | 
			
		||||
        ("#f46a9b", "color 2"),
 | 
			
		||||
        ("#ef9b20", "color 3"),
 | 
			
		||||
        ("#edbf33", "color 4"),
 | 
			
		||||
        ("#ede15b", "color 5"),
 | 
			
		||||
        ("#bdcf32", "color 6"),
 | 
			
		||||
        ("#87bc45", "color 7"),
 | 
			
		||||
        ("#27aeef", "color 8"),
 | 
			
		||||
        ("#b33dc6", "color 9"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    name = models.CharField(
 | 
			
		||||
        verbose_name=_("Name"), help_text=_("Category name"), max_length=512
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    slug = AutoSlugField(null=True, default=None, unique=True, populate_from="name")
 | 
			
		||||
 | 
			
		||||
    color = ColorField(
 | 
			
		||||
        verbose_name=_("Color"),
 | 
			
		||||
        help_text=_("Color used as background for the category"),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    pictogram = models.FileField(
 | 
			
		||||
        verbose_name=_("Pictogram"),
 | 
			
		||||
        help_text=_("Pictogram of the category (svg format)"),
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    position = models.IntegerField(
 | 
			
		||||
        verbose_name=_("Position for ordering categories"), default=0
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if self.color is None:
 | 
			
		||||
            existing_colors = [c.color for c in Category.objects.all()]
 | 
			
		||||
            if len(existing_colors) > len(Category.COLOR_PALETTE):
 | 
			
		||||
                self.color = "#CCCCCC"
 | 
			
		||||
            else:
 | 
			
		||||
                for c, n in Category.COLOR_PALETTE:
 | 
			
		||||
                    if c not in existing_colors:
 | 
			
		||||
                        self.color = c
 | 
			
		||||
                        break
 | 
			
		||||
            if self.color is None:
 | 
			
		||||
                self.color = "#CCCCCC"
 | 
			
		||||
 | 
			
		||||
        super(Category, self).save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_default_category(cls):
 | 
			
		||||
        try:
 | 
			
		||||
            # try to get an existing category
 | 
			
		||||
            default = Category.objects.get(name=Category.default_name)
 | 
			
		||||
 | 
			
		||||
            return default
 | 
			
		||||
        except Exception:
 | 
			
		||||
            # if it does not exist, return it
 | 
			
		||||
            default, created = Category.objects.get_or_create(
 | 
			
		||||
                name=Category.default_name,
 | 
			
		||||
                color=Category.default_color,
 | 
			
		||||
            )
 | 
			
		||||
            return default
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_default_category_id(cls):
 | 
			
		||||
        cat = Category.get_default_category()
 | 
			
		||||
        if cat:
 | 
			
		||||
            return cat.id
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def css_class(self):
 | 
			
		||||
        return "cat-" + str(self.id)
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse("home_category", kwargs={"cat": self.slug})
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Category")
 | 
			
		||||
        verbose_name_plural = _("Categories")
 | 
			
		||||
        indexes = [
 | 
			
		||||
            models.Index(fields=["name"]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CategorisationRule(models.Model):
 | 
			
		||||
    weight = models.IntegerField(
 | 
			
		||||
        verbose_name=_("Weight"),
 | 
			
		||||
        help_text=_("The lower is the weight, the earlier the filter is applied"),
 | 
			
		||||
        default=1,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    category = models.ForeignKey(
 | 
			
		||||
        Category,
 | 
			
		||||
        verbose_name=_("Category"),
 | 
			
		||||
        help_text=_("Category applied to the event"),
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    title_contains = models.CharField(
 | 
			
		||||
        verbose_name=_("Contained in the title"),
 | 
			
		||||
        help_text=_("Text contained in the event title"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    title_exact = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Exact title extract"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "If checked, the extract will be searched for in the title using the exact form (capitals, accents)."
 | 
			
		||||
        ),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    description_contains = models.CharField(
 | 
			
		||||
        verbose_name=_("Contained in the description"),
 | 
			
		||||
        help_text=_("Text contained in the description"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    desc_exact = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Exact description extract"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "If checked, the extract will be searched for in the description using the exact form (capitals, accents)."
 | 
			
		||||
        ),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    location_contains = models.CharField(
 | 
			
		||||
        verbose_name=_("Contained in the location"),
 | 
			
		||||
        help_text=_("Text contained in the event location"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    loc_exact = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Exact location extract"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "If checked, the extract will be searched for in the location using the exact form (capitals, accents)."
 | 
			
		||||
        ),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    place = models.ForeignKey(
 | 
			
		||||
        Place,
 | 
			
		||||
        verbose_name=_("Place"),
 | 
			
		||||
        help_text=_("Location from place"),
 | 
			
		||||
        null=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    rules = None
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Categorisation rule")
 | 
			
		||||
        verbose_name_plural = _("Categorisation rules")
 | 
			
		||||
        permissions = [("apply_categorisationrules", "Apply a categorisation rule")]
 | 
			
		||||
 | 
			
		||||
    # all rules are applied, starting from the first to the last
 | 
			
		||||
    def apply_rules(event):
 | 
			
		||||
        c = CategorisationRule.get_category_from_rules(event)
 | 
			
		||||
 | 
			
		||||
        if c is None:
 | 
			
		||||
            return 0
 | 
			
		||||
        else:
 | 
			
		||||
            event.category = c
 | 
			
		||||
            return 1
 | 
			
		||||
 | 
			
		||||
    def get_category_from_rules(event):
 | 
			
		||||
        cats = defaultdict(lambda: 0)
 | 
			
		||||
        if CategorisationRule.rules is None:
 | 
			
		||||
            CategorisationRule.rules = (
 | 
			
		||||
                CategorisationRule.objects.all()
 | 
			
		||||
                .select_related("category")
 | 
			
		||||
                .select_related("place")
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        for rule in CategorisationRule.rules:
 | 
			
		||||
            if rule.match(event):
 | 
			
		||||
                cats[rule.category] += rule.weight
 | 
			
		||||
 | 
			
		||||
        if len(cats) == 0:
 | 
			
		||||
            return None
 | 
			
		||||
        else:
 | 
			
		||||
            return max(cats, key=cats.get)
 | 
			
		||||
 | 
			
		||||
    def match(self, event):
 | 
			
		||||
        if self.description_contains and self.description_contains != "":
 | 
			
		||||
            if self.desc_exact:
 | 
			
		||||
                result = self.description_contains in event.description
 | 
			
		||||
            else:
 | 
			
		||||
                result = event.description and (
 | 
			
		||||
                    remove_accents(self.description_contains).lower()
 | 
			
		||||
                    in remove_accents(event.description).lower()
 | 
			
		||||
                )
 | 
			
		||||
            if not result:
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
        if self.title_contains and self.title_contains != "":
 | 
			
		||||
            if self.title_exact:
 | 
			
		||||
                result = self.title_contains in event.title
 | 
			
		||||
            else:
 | 
			
		||||
                result = (
 | 
			
		||||
                    remove_accents(self.title_contains).lower()
 | 
			
		||||
                    in remove_accents(event.title).lower()
 | 
			
		||||
                )
 | 
			
		||||
            if not result:
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
        if self.location_contains and self.location_contains != "":
 | 
			
		||||
            if self.loc_exact:
 | 
			
		||||
                result = self.location_contains in event.location
 | 
			
		||||
            else:
 | 
			
		||||
                result = (
 | 
			
		||||
                    remove_accents(self.location_contains).lower()
 | 
			
		||||
                    in remove_accents(event.location).lower()
 | 
			
		||||
                )
 | 
			
		||||
            if not result:
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
        if self.place:
 | 
			
		||||
            if not event.exact_location == self.place:
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
							
								
								
									
										89
									
								
								src/agenda_culturel/models/configuration.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/agenda_culturel/models/configuration.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.templatetags.static import static
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from solo.models import SingletonModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteConfiguration(SingletonModel):
 | 
			
		||||
    site_name = models.CharField(
 | 
			
		||||
        verbose_name=_("Site name"), max_length=255, default="Pommes de lune"
 | 
			
		||||
    )
 | 
			
		||||
    site_url = models.CharField(
 | 
			
		||||
        verbose_name=_("Site url"), max_length=255, default="https://pommesdelune.fr"
 | 
			
		||||
    )
 | 
			
		||||
    site_description = models.CharField(
 | 
			
		||||
        verbose_name=_("Site description"),
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        default="Agenda participatif des sorties culturelles à Clermont-Ferrand et aux environs",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    html_keywords = models.CharField(
 | 
			
		||||
        verbose_name=_("Keywords in html header"),
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
        default="Clermont-Ferrand, Puy-de-Dôme, agenda culturel, agenda participatif, sortir à clermont, sorties, concerts, théâtre, danse, animations, ateliers, lectures",
 | 
			
		||||
    )
 | 
			
		||||
    html_description = models.CharField(
 | 
			
		||||
        verbose_name=_("Description in html header"),
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
        default="Où sortir à Clermont-Ferrand? Retrouve tous les bons plans sur l'agenda participatif des événements culturels à Clermont-Ferrand et dans le Puy-de-Dôme",
 | 
			
		||||
    )
 | 
			
		||||
    google_site_verification = models.CharField(
 | 
			
		||||
        verbose_name=_("Google site verification value"),
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        default=None,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    ms_site_verification = models.CharField(
 | 
			
		||||
        verbose_name=_("Microsoft (bing) site verification value"),
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        default=None,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    favicon = models.ImageField(
 | 
			
		||||
        verbose_name=_("Illustration"),
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    favicon_dev = models.ImageField(
 | 
			
		||||
        verbose_name=_("Illustration (development version)"),
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def guess_mimetype(url):
 | 
			
		||||
        if url.endswith("png"):
 | 
			
		||||
            return "image/png"
 | 
			
		||||
        if url.endswith("jpg"):
 | 
			
		||||
            return "image/jpg"
 | 
			
		||||
        if url.endswith("svg"):
 | 
			
		||||
            return "image/svg+xml"
 | 
			
		||||
        if url.endswith("ico"):
 | 
			
		||||
            return "image/x-icon"
 | 
			
		||||
        return "image/jpg"
 | 
			
		||||
 | 
			
		||||
    def get_favicon_url(self):
 | 
			
		||||
        if settings.DEBUG:
 | 
			
		||||
            if self.favicon_dev:
 | 
			
		||||
                return self.favicon_dev.url
 | 
			
		||||
            else:
 | 
			
		||||
                return static("images/pdl-64-dev.png")
 | 
			
		||||
        else:
 | 
			
		||||
            if self.favicon:
 | 
			
		||||
                return self.favicon.url
 | 
			
		||||
            else:
 | 
			
		||||
                return static("images/pdl-64.png")
 | 
			
		||||
 | 
			
		||||
    def get_favicon_mimetype(self):
 | 
			
		||||
        return SiteConfiguration.guess_mimetype(self.get_favicon_url())
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return str(_("Site Configuration"))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Site Configuration")
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										69
									
								
								src/agenda_culturel/models/import_batch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/agenda_culturel/models/import_batch.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from ..models import RecurrentImport
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BatchImportation(models.Model):
 | 
			
		||||
    class STATUS(models.TextChoices):
 | 
			
		||||
        RUNNING = "running", _("Running")
 | 
			
		||||
        CANCELED = "canceled", _("Canceled")
 | 
			
		||||
        SUCCESS = "success", _("Success")
 | 
			
		||||
        FAILED = "failed", _("Failed")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Batch importation")
 | 
			
		||||
        verbose_name_plural = _("Batch importations")
 | 
			
		||||
        permissions = [("run_batchimportation", "Can run a batch importation")]
 | 
			
		||||
        indexes = [
 | 
			
		||||
            models.Index(fields=["created_date"]),
 | 
			
		||||
            models.Index(fields=["status"]),
 | 
			
		||||
            models.Index(fields=["created_date", "recurrentImport"]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    created_date = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
 | 
			
		||||
    recurrentImport = models.ForeignKey(
 | 
			
		||||
        RecurrentImport,
 | 
			
		||||
        verbose_name=_("Recurrent import"),
 | 
			
		||||
        help_text=_("Reference to the recurrent import processing"),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        editable=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    url_source = models.URLField(
 | 
			
		||||
        verbose_name=_("URL (if not recurrent import)"),
 | 
			
		||||
        help_text=_("Source URL if no RecurrentImport is associated."),
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
        editable=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        _("Status"),
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=STATUS.choices,
 | 
			
		||||
        default=STATUS.RUNNING,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    error_message = models.CharField(
 | 
			
		||||
        verbose_name=_("Error message"), max_length=512, blank=True, null=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    nb_initial = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_("Number of collected events"), default=0
 | 
			
		||||
    )
 | 
			
		||||
    nb_imported = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_("Number of imported events"), default=0
 | 
			
		||||
    )
 | 
			
		||||
    nb_updated = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_("Number of updated events"), default=0
 | 
			
		||||
    )
 | 
			
		||||
    nb_removed = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_("Number of removed events"), default=0
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    celery_id = models.CharField(max_length=128, default="")
 | 
			
		||||
							
								
								
									
										165
									
								
								src/agenda_culturel/models/import_recurrent.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								src/agenda_culturel/models/import_recurrent.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_better_admin_arrayfield.models.fields import ArrayField
 | 
			
		||||
 | 
			
		||||
from ..models import Organisation, Category, BatchImportation, Event
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecurrentImport(models.Model):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Recurrent import")
 | 
			
		||||
        verbose_name_plural = _("Recurrent imports")
 | 
			
		||||
        permissions = [("run_recurrentimport", "Can run a recurrent import")]
 | 
			
		||||
 | 
			
		||||
    class PROCESSOR(models.TextChoices):
 | 
			
		||||
        ICAL = "ical", _("ical")
 | 
			
		||||
        ICALNOBUSY = "icalnobusy", _("ical no busy")
 | 
			
		||||
        ICALNOVC = "icalnovc", _("ical no VC")
 | 
			
		||||
        ICALNAIVETZ = "ical naive tz", _("ical naive timezone")
 | 
			
		||||
        LACOOPE = "lacoope", _("lacoope.org")
 | 
			
		||||
        LACOMEDIE = "lacomedie", _("la comédie")
 | 
			
		||||
        LEFOTOMAT = "lefotomat", _("le fotomat")
 | 
			
		||||
        LAPUCEALOREILLE = "lapucealoreille", _("la puce à l'oreille")
 | 
			
		||||
        MECWORDPRESS = "Plugin wordpress MEC", _("Plugin wordpress MEC")
 | 
			
		||||
        FBEVENTS = "Facebook events", _("Événements d'une page FB")
 | 
			
		||||
        BILLETTERIECF = "Billetterie CF", _("Billetterie Clermont-Ferrand")
 | 
			
		||||
        ARACHNEE = "arachnee", _("Arachnée concert")
 | 
			
		||||
        LERIO = "rio", _("Le Rio")
 | 
			
		||||
        LARAYMONDE = "raymonde", _("La Raymonde")
 | 
			
		||||
        APIDAE = "apidae", _("Agenda apidae tourisme")
 | 
			
		||||
        IGUANA = "iguana", _("Agenda iguana (médiathèques)")
 | 
			
		||||
        MILLEFORMES = "Mille formes", _("Mille formes")
 | 
			
		||||
        AMISCERISES = "Amis cerises", _("Les Amis du Temps des Cerises")
 | 
			
		||||
        MOBILIZON = "Mobilizon", _("Mobilizon")
 | 
			
		||||
        LECAMELEON = "Le Caméléon", _("Le caméléon")
 | 
			
		||||
        ECHOSCIENCES = "Echosciences", _("Echosciences")
 | 
			
		||||
        HELLOASSO = "HelloAsso", _("Hello Asso")
 | 
			
		||||
 | 
			
		||||
    class DOWNLOADER(models.TextChoices):
 | 
			
		||||
        SIMPLE = "simple", _("simple")
 | 
			
		||||
        CHROMIUMHEADLESS = "chromium headless", _("Headless Chromium")
 | 
			
		||||
        CHROMIUMHEADLESSPAUSE = (
 | 
			
		||||
            "chromium (pause)",
 | 
			
		||||
            _("Headless Chromium (pause)"),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class RECURRENCE(models.TextChoices):
 | 
			
		||||
        DAILY = (
 | 
			
		||||
            "daily",
 | 
			
		||||
            _("daily"),
 | 
			
		||||
        )
 | 
			
		||||
        WEEKLY = "weekly", _("weekly")
 | 
			
		||||
        NEVER = "never", _("never")
 | 
			
		||||
 | 
			
		||||
    name = models.CharField(
 | 
			
		||||
        verbose_name=_("Name"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Recurrent import name. Be careful to choose a name that is easy to understand, as it will be public and displayed on the site"
 | 
			
		||||
            "s About page."
 | 
			
		||||
        ),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        default="",
 | 
			
		||||
    )
 | 
			
		||||
    processor = models.CharField(
 | 
			
		||||
        _("Processor"),
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=PROCESSOR.choices,
 | 
			
		||||
        default=PROCESSOR.ICAL,
 | 
			
		||||
    )
 | 
			
		||||
    downloader = models.CharField(
 | 
			
		||||
        _("Downloader"),
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=DOWNLOADER.choices,
 | 
			
		||||
        default=DOWNLOADER.SIMPLE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    recurrence = models.CharField(
 | 
			
		||||
        _("Import recurrence"),
 | 
			
		||||
        max_length=10,
 | 
			
		||||
        choices=RECURRENCE.choices,
 | 
			
		||||
        default=RECURRENCE.DAILY,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    source = models.URLField(
 | 
			
		||||
        verbose_name=_("Source"),
 | 
			
		||||
        help_text=_("URL of the source document"),
 | 
			
		||||
        unique=True,
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
    )
 | 
			
		||||
    browsable_url = models.URLField(
 | 
			
		||||
        verbose_name=_("Browsable url"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "URL of the corresponding document that will be shown to visitors."
 | 
			
		||||
        ),
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    defaultPublished = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Published"),
 | 
			
		||||
        help_text=_("Status of each imported event (published or draft)"),
 | 
			
		||||
        default=True,
 | 
			
		||||
    )
 | 
			
		||||
    defaultLocation = models.CharField(
 | 
			
		||||
        verbose_name=_("Location"),
 | 
			
		||||
        help_text=_("Address for each imported event"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    forceLocation = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Force location"),
 | 
			
		||||
        help_text=_("force location even if another is detected."),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    defaultOrganiser = models.ForeignKey(
 | 
			
		||||
        Organisation,
 | 
			
		||||
        verbose_name=_("Organiser"),
 | 
			
		||||
        help_text=_("Organiser of each imported event"),
 | 
			
		||||
        default=None,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        on_delete=models.SET_DEFAULT,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    defaultCategory = models.ForeignKey(
 | 
			
		||||
        Category,
 | 
			
		||||
        verbose_name=_("Category"),
 | 
			
		||||
        help_text=_("Category of each imported event"),
 | 
			
		||||
        default=None,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        on_delete=models.SET_DEFAULT,
 | 
			
		||||
    )
 | 
			
		||||
    defaultTags = ArrayField(
 | 
			
		||||
        models.CharField(max_length=64),
 | 
			
		||||
        verbose_name=_("Tags for each imported event"),
 | 
			
		||||
        help_text=_("A list of tags that describe each imported event."),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def nb_imports(self):
 | 
			
		||||
        return BatchImportation.objects.filter(recurrentImport=self).count()
 | 
			
		||||
 | 
			
		||||
    def nb_events(self):
 | 
			
		||||
        return Event.objects.filter(import_sources__contains=[self.source]).count()
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse("view_rimport", kwargs={"pk": self.pk})
 | 
			
		||||
 | 
			
		||||
    def last_import(self):
 | 
			
		||||
        events = BatchImportation.objects.filter(recurrentImport=self).order_by(
 | 
			
		||||
            "-created_date"
 | 
			
		||||
        )
 | 
			
		||||
        if events and len(events) > 0:
 | 
			
		||||
            return events[0]
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
							
								
								
									
										121
									
								
								src/agenda_culturel/models/message.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/agenda_culturel/models/message.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_ckeditor_5.fields import CKEditor5Field
 | 
			
		||||
 | 
			
		||||
from ..models import Event
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Message(models.Model):
 | 
			
		||||
    class TYPE(models.TextChoices):
 | 
			
		||||
        FROM_CONTRIBUTOR = "from_contributor", _("From contributor")
 | 
			
		||||
        IMPORT_PROCESS = "import_process", _("Import process")
 | 
			
		||||
        UPDATE_PROCESS = "update_process", _("Update process")
 | 
			
		||||
        CONTACT_FORM = "contact_form", _("Contact form")
 | 
			
		||||
        EVENT_REPORT = "event_report", _("Event report")
 | 
			
		||||
        FROM_CONTRIBUTOR_NO_MSG = (
 | 
			
		||||
            "from_contrib_no_msg",
 | 
			
		||||
            _("From contributor (without message)"),
 | 
			
		||||
        )
 | 
			
		||||
        WARNING = "warning", _("Warning")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Message")
 | 
			
		||||
        verbose_name_plural = _("Messages")
 | 
			
		||||
        indexes = [
 | 
			
		||||
            models.Index(fields=["related_event"]),
 | 
			
		||||
            models.Index(fields=["user"]),
 | 
			
		||||
            models.Index(fields=["date"]),
 | 
			
		||||
            models.Index(fields=["spam", "closed"]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    subject = models.CharField(
 | 
			
		||||
        verbose_name=_("Subject"),
 | 
			
		||||
        help_text=_("The subject of your message"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    related_event = models.ForeignKey(
 | 
			
		||||
        Event,
 | 
			
		||||
        verbose_name=_("Related event"),
 | 
			
		||||
        help_text=_("The message is associated with this event."),
 | 
			
		||||
        null=True,
 | 
			
		||||
        default=None,
 | 
			
		||||
        on_delete=models.SET_DEFAULT,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    user = models.ForeignKey(
 | 
			
		||||
        User,
 | 
			
		||||
        verbose_name=_("Author of the message"),
 | 
			
		||||
        null=True,
 | 
			
		||||
        default=None,
 | 
			
		||||
        on_delete=models.SET_DEFAULT,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    name = models.CharField(
 | 
			
		||||
        verbose_name=_("Name"),
 | 
			
		||||
        help_text=_("Your name"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    email = models.EmailField(
 | 
			
		||||
        verbose_name=_("Email address"),
 | 
			
		||||
        help_text=_("Your email address"),
 | 
			
		||||
        max_length=254,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    message = CKEditor5Field(
 | 
			
		||||
        verbose_name=_("Message"), help_text=_("Your message"), blank=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    date = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
 | 
			
		||||
    spam = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Spam"),
 | 
			
		||||
        help_text=_("This message is a spam."),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    closed = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Closed"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "this message has been processed and no longer needs to be handled"
 | 
			
		||||
        ),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
    comments = CKEditor5Field(
 | 
			
		||||
        verbose_name=_("Comments"),
 | 
			
		||||
        help_text=_("Comments on the message from the moderation team"),
 | 
			
		||||
        default="",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    message_type = models.CharField(
 | 
			
		||||
        verbose_name=_("Type"),
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=TYPE.choices,
 | 
			
		||||
        default=None,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def nb_open_messages(cls):
 | 
			
		||||
        return Message.objects.filter(
 | 
			
		||||
            Q(closed=False)
 | 
			
		||||
            & Q(spam=False)
 | 
			
		||||
            & Q(
 | 
			
		||||
                message_type__in=[
 | 
			
		||||
                    Message.TYPE.CONTACT_FORM,
 | 
			
		||||
                    Message.TYPE.EVENT_REPORT,
 | 
			
		||||
                    Message.TYPE.FROM_CONTRIBUTOR,
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
        ).count()
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse("message", kwargs={"pk": self.pk})
 | 
			
		||||
							
								
								
									
										52
									
								
								src/agenda_culturel/models/organisation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/agenda_culturel/models/organisation.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_ckeditor_5.fields import CKEditor5Field
 | 
			
		||||
 | 
			
		||||
from ..models import Place
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Organisation(models.Model):
 | 
			
		||||
    name = models.CharField(
 | 
			
		||||
        verbose_name=_("Name"),
 | 
			
		||||
        help_text=_("Organisation name"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        null=False,
 | 
			
		||||
        unique=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    website = models.URLField(
 | 
			
		||||
        verbose_name=_("Website"),
 | 
			
		||||
        help_text=_("Website of the organisation"),
 | 
			
		||||
        max_length=1024,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    description = CKEditor5Field(
 | 
			
		||||
        verbose_name=_("Description"),
 | 
			
		||||
        help_text=_("Description of the organisation."),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    principal_place = models.ForeignKey(
 | 
			
		||||
        Place,
 | 
			
		||||
        verbose_name=_("Principal place"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Place mainly associated with this organizer. Mainly used if there is a similarity in the name, to avoid redundant displays."
 | 
			
		||||
        ),
 | 
			
		||||
        null=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Organisation")
 | 
			
		||||
        verbose_name_plural = _("Organisations")
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse("view_organisation", kwargs={"pk": self.pk, "extra": self.name})
 | 
			
		||||
							
								
								
									
										161
									
								
								src/agenda_culturel/models/place.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/agenda_culturel/models/place.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
from django.contrib.gis.geos import Point
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.template.defaultfilters import slugify
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_better_admin_arrayfield.models.fields import ArrayField
 | 
			
		||||
from django_ckeditor_5.fields import CKEditor5Field
 | 
			
		||||
from django_extensions.db.fields import AutoSlugField
 | 
			
		||||
from location_field.models.spatial import LocationField
 | 
			
		||||
 | 
			
		||||
from ..models import Event
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Place(models.Model):
 | 
			
		||||
    name = models.CharField(verbose_name=_("Name"), help_text=_("Name of the place"))
 | 
			
		||||
    address = models.CharField(
 | 
			
		||||
        verbose_name=_("Address"),
 | 
			
		||||
        help_text=_("Address of this place (without city name)"),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    postcode = models.CharField(
 | 
			
		||||
        verbose_name=_("Postcode"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "The post code is not displayed, but makes it easier to find an address when you enter it."
 | 
			
		||||
        ),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
    city = models.CharField(verbose_name=_("City"), help_text=_("City name"))
 | 
			
		||||
    location = LocationField(
 | 
			
		||||
        based_fields=["name", "address", "postcode", "city"],
 | 
			
		||||
        zoom=12,
 | 
			
		||||
        default=Point(3.08333, 45.783329),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    description = CKEditor5Field(
 | 
			
		||||
        verbose_name=_("Description"),
 | 
			
		||||
        help_text=_("Description of the place, including accessibility."),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    aliases = ArrayField(
 | 
			
		||||
        models.CharField(max_length=512),
 | 
			
		||||
        verbose_name=_("Alternative names"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Alternative names or addresses used to match a place with the free-form location of an event."
 | 
			
		||||
        ),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Place")
 | 
			
		||||
        verbose_name_plural = _("Places")
 | 
			
		||||
        ordering = ["name"]
 | 
			
		||||
        indexes = [
 | 
			
		||||
            models.Index(fields=["name"]),
 | 
			
		||||
            models.Index(fields=["city"]),
 | 
			
		||||
            models.Index(fields=["location"]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        if self.address:
 | 
			
		||||
            return self.name + ", " + self.address + ", " + self.city
 | 
			
		||||
        else:
 | 
			
		||||
            return self.name + ", " + self.city
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse(
 | 
			
		||||
            "view_place_fullname",
 | 
			
		||||
            kwargs={"pk": self.pk, "extra": slugify(self.name)},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def nb_events(self):
 | 
			
		||||
        return Event.objects.filter(exact_location=self).count()
 | 
			
		||||
 | 
			
		||||
    def nb_events_future(self):
 | 
			
		||||
        return (
 | 
			
		||||
            Event.objects.filter(start_day__gte=datetime.now())
 | 
			
		||||
            .filter(exact_location=self)
 | 
			
		||||
            .count()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def match(self, event):
 | 
			
		||||
        if self.aliases and event.location:
 | 
			
		||||
            return event.location.strip() in self.aliases
 | 
			
		||||
        else:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def associate_matching_events(self):
 | 
			
		||||
        u_events = Event.objects.filter(exact_location__isnull=True)
 | 
			
		||||
 | 
			
		||||
        to_be_updated = []
 | 
			
		||||
        # try to find matches
 | 
			
		||||
        for ue in u_events:
 | 
			
		||||
            if self.match(ue):
 | 
			
		||||
                ue.exact_location = self
 | 
			
		||||
                to_be_updated.append(ue)
 | 
			
		||||
                continue
 | 
			
		||||
        # update events with a location
 | 
			
		||||
        Event.objects.bulk_update(to_be_updated, fields=["exact_location"])
 | 
			
		||||
        return len(to_be_updated)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_all_cities(cls):
 | 
			
		||||
        try:
 | 
			
		||||
            tags = list(
 | 
			
		||||
                [
 | 
			
		||||
                    p["city"]
 | 
			
		||||
                    for p in Place.objects.values("city").distinct().order_by("city")
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
        except Exception:
 | 
			
		||||
            tags = []
 | 
			
		||||
        return tags
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReferenceLocation(models.Model):
 | 
			
		||||
    name = models.CharField(
 | 
			
		||||
        verbose_name=_("Name"),
 | 
			
		||||
        help_text=_("Name of the location"),
 | 
			
		||||
        unique=True,
 | 
			
		||||
        null=False,
 | 
			
		||||
    )
 | 
			
		||||
    location = LocationField(
 | 
			
		||||
        based_fields=["name"],
 | 
			
		||||
        zoom=12,
 | 
			
		||||
        default=Point(3.08333, 45.783329),
 | 
			
		||||
        srid=4326,
 | 
			
		||||
    )
 | 
			
		||||
    main = models.IntegerField(
 | 
			
		||||
        verbose_name=_("Main"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "This location is one of the main locations (shown first higher values)."
 | 
			
		||||
        ),
 | 
			
		||||
        default=0,
 | 
			
		||||
    )
 | 
			
		||||
    suggested_distance = models.IntegerField(
 | 
			
		||||
        verbose_name=_("Suggested distance (km)"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "If this distance is given, this location is part of the suggested filters."
 | 
			
		||||
        ),
 | 
			
		||||
        null=True,
 | 
			
		||||
        default=None,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    slug = AutoSlugField(null=True, default=None, unique=True, populate_from="name")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Reference location")
 | 
			
		||||
        verbose_name_plural = _("Reference locations")
 | 
			
		||||
        indexes = [
 | 
			
		||||
            models.Index(fields=["name"]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
							
								
								
									
										109
									
								
								src/agenda_culturel/models/special_period.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/agenda_culturel/models/special_period.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
from datetime import date
 | 
			
		||||
 | 
			
		||||
import icalendar
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.utils.timezone import datetime
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecialPeriod(models.Model):
 | 
			
		||||
 | 
			
		||||
    name = models.CharField(verbose_name=_("Period name"), max_length=255)
 | 
			
		||||
    start_date = models.DateField(verbose_name=_("Start day"))
 | 
			
		||||
    end_date = models.DateField(verbose_name=_("End day"))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Special period")
 | 
			
		||||
        verbose_name_plural = _("Special periods")
 | 
			
		||||
        indexes = [
 | 
			
		||||
            models.Index(fields=["start_date", "end_date"]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    class PERIODTYPE(models.TextChoices):
 | 
			
		||||
        PUBLICHOLIDAYS = "public holidays", _("public holidays")
 | 
			
		||||
        SCHOOLVACATIONS = "school vacations", _("school vacations")
 | 
			
		||||
 | 
			
		||||
    periodtype = models.CharField(
 | 
			
		||||
        _("Period type"),
 | 
			
		||||
        max_length=20,
 | 
			
		||||
        choices=PERIODTYPE.choices,
 | 
			
		||||
        default=PERIODTYPE.PUBLICHOLIDAYS,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        # Call the parent class's clean() method
 | 
			
		||||
        super().clean()
 | 
			
		||||
 | 
			
		||||
        # Check that the end date is after or equal to the start date
 | 
			
		||||
        if self.end_date < self.start_date:
 | 
			
		||||
            raise ValidationError(
 | 
			
		||||
                {
 | 
			
		||||
                    "end_date": _(
 | 
			
		||||
                        "The end date must be after or equal to the start date."
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        n = self.periodtype + ' "' + self.name + '"'
 | 
			
		||||
        if self.start_date == self.end_date:
 | 
			
		||||
            return n + _(" on ") + str(self.start_date)
 | 
			
		||||
        else:
 | 
			
		||||
            return (
 | 
			
		||||
                n + _(" from ") + str(self.start_date) + _(" to ") + str(self.end_date)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def load_from_ical(uploaded_file, periodtype):
 | 
			
		||||
        # load file
 | 
			
		||||
        try:
 | 
			
		||||
            file_content = uploaded_file.read()
 | 
			
		||||
            calendar = icalendar.Calendar.from_ical(file_content)
 | 
			
		||||
 | 
			
		||||
            nb_error = 0
 | 
			
		||||
            nb_overlap = 0
 | 
			
		||||
            periods = []
 | 
			
		||||
            periods_to_create = []
 | 
			
		||||
 | 
			
		||||
            # extract events
 | 
			
		||||
            for event in calendar.walk("VEVENT"):
 | 
			
		||||
                try:
 | 
			
		||||
                    name = event.decoded("SUMMARY").decode()
 | 
			
		||||
                    r = event.decoded("DTSTART")
 | 
			
		||||
                    if isinstance(r, datetime):
 | 
			
		||||
                        start_date = r.date()
 | 
			
		||||
                    elif isinstance(r, date):
 | 
			
		||||
                        start_date = r
 | 
			
		||||
                    r = event.decoded("DTEND")
 | 
			
		||||
                    if isinstance(r, datetime):
 | 
			
		||||
                        end_date = r.date()
 | 
			
		||||
                    elif isinstance(r, date):
 | 
			
		||||
                        end_date = r
 | 
			
		||||
                    periods.append(
 | 
			
		||||
                        SpecialPeriod(
 | 
			
		||||
                            name=name,
 | 
			
		||||
                            periodtype=periodtype,
 | 
			
		||||
                            start_date=start_date,
 | 
			
		||||
                            end_date=end_date,
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                except Exception:
 | 
			
		||||
                    nb_error += 1
 | 
			
		||||
 | 
			
		||||
            for p in periods:
 | 
			
		||||
                overlap_exists = SpecialPeriod.objects.filter(
 | 
			
		||||
                    Q(periodtype=p.periodtype)
 | 
			
		||||
                    & Q(start_date__lte=p.end_date)
 | 
			
		||||
                    & Q(end_date__gte=p.start_date)
 | 
			
		||||
                ).exists()
 | 
			
		||||
 | 
			
		||||
                if overlap_exists:
 | 
			
		||||
                    nb_overlap += 1
 | 
			
		||||
                else:
 | 
			
		||||
                    periods_to_create.append(p)
 | 
			
		||||
 | 
			
		||||
            SpecialPeriod.objects.bulk_create(periods_to_create)
 | 
			
		||||
            return len(periods_to_create), nb_overlap, nb_error, None
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return 0, 0, 0, str(e)
 | 
			
		||||
							
								
								
									
										34
									
								
								src/agenda_culturel/models/static_content.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/agenda_culturel/models/static_content.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_ckeditor_5.fields import CKEditor5Field
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StaticContent(models.Model):
 | 
			
		||||
    name = models.CharField(
 | 
			
		||||
        verbose_name=_("Name"),
 | 
			
		||||
        help_text=_("Category name"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        unique=True,
 | 
			
		||||
    )
 | 
			
		||||
    text = CKEditor5Field(
 | 
			
		||||
        verbose_name=_("Content"),
 | 
			
		||||
        help_text=_("Text as shown to the visitors"),
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    url_path = models.CharField(
 | 
			
		||||
        verbose_name=_("URL path"),
 | 
			
		||||
        help_text=_("URL path where the content is included."),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Static content")
 | 
			
		||||
        verbose_name_plural = _("Static contents")
 | 
			
		||||
        indexes = [
 | 
			
		||||
            models.Index(fields=["name"]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return self.url_path
 | 
			
		||||
							
								
								
									
										158
									
								
								src/agenda_culturel/models/tag.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								src/agenda_culturel/models/tag.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,158 @@
 | 
			
		||||
import hashlib
 | 
			
		||||
 | 
			
		||||
import emoji
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_ckeditor_5.fields import CKEditor5Field
 | 
			
		||||
 | 
			
		||||
from ..models import Event, no_slash_validator, remove_accents
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Tag(models.Model):
 | 
			
		||||
    name = models.CharField(
 | 
			
		||||
        verbose_name=_("Name"),
 | 
			
		||||
        help_text=_("Tag name"),
 | 
			
		||||
        max_length=512,
 | 
			
		||||
        unique=True,
 | 
			
		||||
        validators=[no_slash_validator],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    description = CKEditor5Field(
 | 
			
		||||
        verbose_name=_("Description"),
 | 
			
		||||
        help_text=_("Description of the tag"),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    message = CKEditor5Field(
 | 
			
		||||
        verbose_name=_("Message"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Message displayed to the user on each event associated with this tag."
 | 
			
		||||
        ),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    principal = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Principal"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "This tag is highlighted as a main tag for visitors, particularly in the filter."
 | 
			
		||||
        ),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    in_excluded_suggestions = models.BooleanField(
 | 
			
		||||
        verbose_name=_("In excluded suggestions"),
 | 
			
		||||
        help_text=_("This tag will be part of the excluded suggestions."),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    in_included_suggestions = models.BooleanField(
 | 
			
		||||
        verbose_name=_("In included suggestions"),
 | 
			
		||||
        help_text=_("This tag will be part of the included suggestions."),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Tag")
 | 
			
		||||
        verbose_name_plural = _("Tags")
 | 
			
		||||
        indexes = [
 | 
			
		||||
            models.Index(fields=["name"]),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse("view_tag", kwargs={"t": self.name})
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def clear_cache(cls):
 | 
			
		||||
        for exclude in [False, True]:
 | 
			
		||||
            for include in [False, True]:
 | 
			
		||||
                for nb_suggestions in [10]:
 | 
			
		||||
                    id_cache = (
 | 
			
		||||
                        "all_tags "
 | 
			
		||||
                        + str(exclude)
 | 
			
		||||
                        + " "
 | 
			
		||||
                        + str(include)
 | 
			
		||||
                        + " "
 | 
			
		||||
                        + str(nb_suggestions)
 | 
			
		||||
                    )
 | 
			
		||||
                    id_cache = hashlib.md5(id_cache.encode("utf8")).hexdigest()
 | 
			
		||||
                    cache.delete(id_cache)
 | 
			
		||||
 | 
			
		||||
    def get_tag_groups(nb_suggestions=10, exclude=False, include=False, all=False):
 | 
			
		||||
        id_cache = (
 | 
			
		||||
            "all_tags " + str(exclude) + " " + str(include) + " " + str(nb_suggestions)
 | 
			
		||||
        )
 | 
			
		||||
        id_cache = hashlib.md5(id_cache.encode("utf8")).hexdigest()
 | 
			
		||||
        result = cache.get(id_cache)
 | 
			
		||||
 | 
			
		||||
        if not result:
 | 
			
		||||
            free_tags = Event.get_all_tags(False)
 | 
			
		||||
            f_tags = [t["tag"] for t in free_tags]
 | 
			
		||||
 | 
			
		||||
            obj_tags = Tag.objects
 | 
			
		||||
 | 
			
		||||
            if all:
 | 
			
		||||
                obj_tags = obj_tags.filter(
 | 
			
		||||
                    Q(in_excluded_suggestions=True)
 | 
			
		||||
                    | Q(in_included_suggestions=True)
 | 
			
		||||
                    | Q(principal=True)
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                if exclude:
 | 
			
		||||
                    obj_tags = obj_tags.filter(Q(in_excluded_suggestions=True))
 | 
			
		||||
                if include:
 | 
			
		||||
                    obj_tags = obj_tags.filter(
 | 
			
		||||
                        Q(in_included_suggestions=True) | Q(principal=True)
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                if not exclude and not include:
 | 
			
		||||
                    obj_tags = obj_tags.filter(principal=True)
 | 
			
		||||
 | 
			
		||||
            obj_tags = obj_tags.values_list("name", flat=True)
 | 
			
		||||
 | 
			
		||||
            if len(obj_tags) > nb_suggestions:
 | 
			
		||||
                nb_suggestions = len(obj_tags)
 | 
			
		||||
 | 
			
		||||
            tags = [
 | 
			
		||||
                {
 | 
			
		||||
                    "tag": t["tag"],
 | 
			
		||||
                    "count": 1000000 if t["tag"] in obj_tags else t["count"],
 | 
			
		||||
                }
 | 
			
		||||
                for t in free_tags
 | 
			
		||||
            ]
 | 
			
		||||
            tags += [
 | 
			
		||||
                {"tag": o, "count": 0}
 | 
			
		||||
                for o in Tag.objects.filter(~Q(name__in=f_tags)).values_list(
 | 
			
		||||
                    "name", flat=True
 | 
			
		||||
                )
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
            tags.sort(key=lambda x: -x["count"])
 | 
			
		||||
 | 
			
		||||
            tags1 = tags[0:nb_suggestions]
 | 
			
		||||
            tags1.sort(
 | 
			
		||||
                key=lambda x: emoji.demojize(
 | 
			
		||||
                    remove_accents(x["tag"]).lower(), delimiters=("000", "")
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            tags2 = tags[nb_suggestions:]
 | 
			
		||||
            tags2.sort(
 | 
			
		||||
                key=lambda x: emoji.demojize(
 | 
			
		||||
                    remove_accents(x["tag"]).lower(), delimiters=("000", "")
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            result = (
 | 
			
		||||
                (_("Suggestions"), [(t["tag"], t["tag"]) for t in tags1]),
 | 
			
		||||
                (_("Others"), [(t["tag"], t["tag"]) for t in tags2]),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            cache.set(id_cache, result, 3000)  # 50mn
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
							
								
								
									
										36
									
								
								src/agenda_culturel/models/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/agenda_culturel/models/user.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.signals import post_save
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserProfile(models.Model):
 | 
			
		||||
    user = models.OneToOneField(
 | 
			
		||||
        User,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        primary_key=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    is_moderation_expert = models.BooleanField(
 | 
			
		||||
        verbose_name=_("Expert moderation user"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "This user is an expert in moderation, and the interface features additional functionalities."
 | 
			
		||||
        ),
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("User profile")
 | 
			
		||||
        verbose_name_plural = _("User profiles")
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return _("User profile") + " (" + self.user.username + ")"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=User)
 | 
			
		||||
def update_profile_signal(sender, instance, created, **kwargs):
 | 
			
		||||
    if not hasattr(instance, "userprofile"):
 | 
			
		||||
        UserProfile.objects.create(user=instance)
 | 
			
		||||
 | 
			
		||||
    instance.userprofile.save()
 | 
			
		||||
							
								
								
									
										19
									
								
								src/agenda_culturel/models/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/agenda_culturel/models/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import unicodedata
 | 
			
		||||
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
# Useful for translation
 | 
			
		||||
to_be_translated = [_("mean"), _("median"), _("maximum"), _("minimum"), _("stdev")]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def no_slash_validator(value):
 | 
			
		||||
    if "/" in value:
 | 
			
		||||
        raise ValidationError(_("The '/' character is not allowed."))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def remove_accents(input_str):
 | 
			
		||||
    if input_str is None:
 | 
			
		||||
        return None
 | 
			
		||||
    nfkd_form = unicodedata.normalize("NFKD", input_str)
 | 
			
		||||
    return "".join([c for c in nfkd_form if not unicodedata.combining(c)])
 | 
			
		||||
		Reference in New Issue
	
	Block a user