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