From 3daadf2b86904b0d47ad44267a5898804a2da121 Mon Sep 17 00:00:00 2001 From: SebF Date: Thu, 2 May 2024 11:25:54 +0200 Subject: [PATCH] formatage black sur les src --- src/agenda_culturel/admin.py | 18 +- src/agenda_culturel/calendar.py | 66 +- src/agenda_culturel/celery.py | 41 +- src/agenda_culturel/db_importer.py | 26 +- src/agenda_culturel/forms.py | 306 +++-- .../custom_extractors/__init__.py | 5 +- .../custom_extractors/lacomedie.py | 69 +- .../import_tasks/custom_extractors/lacoope.py | 55 +- .../custom_extractors/lapucealoreille.py | 39 +- .../custom_extractors/lefotomat.py | 36 +- .../import_tasks/downloader.py | 10 +- src/agenda_culturel/import_tasks/extractor.py | 75 +- .../import_tasks/extractor_facebook.py | 159 ++- .../import_tasks/extractor_ical.py | 144 ++- .../import_tasks/generic_extractors.py | 113 +- src/agenda_culturel/import_tasks/importer.py | 24 +- src/agenda_culturel/models.py | 868 ++++++++++---- src/agenda_culturel/settings/base.py | 50 +- src/agenda_culturel/settings/production.py | 4 +- src/agenda_culturel/templatetags/cat_extra.py | 138 ++- .../templatetags/contactmessages_extra.py | 18 +- .../templatetags/duplicated_extra.py | 19 +- .../templatetags/event_extra.py | 62 +- .../templatetags/rimports_extra.py | 34 +- .../templatetags/static_content_extra.py | 4 +- src/agenda_culturel/templatetags/tag_extra.py | 21 +- .../templatetags/utils_extra.py | 33 +- src/agenda_culturel/urls.py | 163 ++- src/agenda_culturel/views.py | 1061 +++++++++++------ 29 files changed, 2627 insertions(+), 1034 deletions(-) diff --git a/src/agenda_culturel/admin.py b/src/agenda_culturel/admin.py index c1aa90d..8727544 100644 --- a/src/agenda_culturel/admin.py +++ b/src/agenda_culturel/admin.py @@ -1,6 +1,14 @@ from django.contrib import admin from django import forms -from .models import Event, Category, StaticContent, DuplicatedEvents, BatchImportation, RecurrentImport, Place +from .models import ( + Event, + Category, + StaticContent, + DuplicatedEvents, + BatchImportation, + RecurrentImport, + Place, +) from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget from django_better_admin_arrayfield.models.fields import DynamicArrayField @@ -16,12 +24,12 @@ admin.site.register(Place) class URLWidget(DynamicArrayWidget): def __init__(self, *args, **kwargs): - kwargs['subwidget_form'] = forms.URLField() + kwargs["subwidget_form"] = forms.URLField() super().__init__(*args, **kwargs) + @admin.register(Event) class Eventdmin(admin.ModelAdmin, DynamicArrayMixin): - formfield_overrides = { - DynamicArrayField: {'urls': URLWidget}, - } \ No newline at end of file + DynamicArrayField: {"urls": URLWidget}, + } diff --git a/src/agenda_culturel/calendar.py b/src/agenda_culturel/calendar.py index 7e064fc..dff1816 100644 --- a/src/agenda_culturel/calendar.py +++ b/src/agenda_culturel/calendar.py @@ -5,6 +5,7 @@ from django.utils import timezone import logging + logger = logging.getLogger(__name__) @@ -21,7 +22,7 @@ def daterange(start, end, step=timedelta(1)): class DayInCalendar: midnight = time(23, 59, 59) - def __init__(self, d, on_requested_interval = True): + def __init__(self, d, on_requested_interval=True): self.date = d now = date.today() self.week = d.isocalendar()[1] @@ -35,6 +36,7 @@ class DayInCalendar: def is_in_past(self): return self.in_past + def is_today(self): return self.today @@ -58,8 +60,9 @@ class DayInCalendar: if removed: # remove empty events_by_category - self.events_by_category = dict([(k, v) for k, v in self.events_by_category.items() if len(v) > 0]) - + self.events_by_category = dict( + [(k, v) for k, v in self.events_by_category.items() if len(v) > 0] + ) def add_event(self, event): if event.contains_date(self.date): @@ -70,24 +73,26 @@ class DayInCalendar: self.remove_event_with_ancestor_uuid_if_exists(event) self._add_event_internal(event) - def _add_event_internal(self, event): - self.events.append(event) - if event.category is None: - if not "" in self.events_by_category: - self.events_by_category[""] = [] - self.events_by_category[""].append(event) - else: - if not event.category.name in self.events_by_category: - self.events_by_category[event.category.name] = [] - self.events_by_category[event.category.name].append(event) + self.events.append(event) + if event.category is None: + if not "" in self.events_by_category: + self.events_by_category[""] = [] + self.events_by_category[""].append(event) + else: + if not event.category.name in self.events_by_category: + self.events_by_category[event.category.name] = [] + self.events_by_category[event.category.name].append(event) def filter_events(self): - self.events.sort(key=lambda e: DayInCalendar.midnight if e.start_time is None else e.start_time) + self.events.sort( + key=lambda e: DayInCalendar.midnight + if e.start_time is None + else e.start_time + ) class CalendarList: - def __init__(self, firstdate, lastdate, filter=None, exact=False): self.firstdate = firstdate self.lastdate = lastdate @@ -101,8 +106,7 @@ class CalendarList: # start the first day of the first week self.c_firstdate = firstdate + timedelta(days=-firstdate.weekday()) # end the last day of the last week - self.c_lastdate = lastdate + timedelta(days=6-lastdate.weekday()) - + self.c_lastdate = lastdate + timedelta(days=6 - lastdate.weekday()) # create a list of DayInCalendars self.create_calendar_days() @@ -114,7 +118,6 @@ class CalendarList: for i, c in self.calendar_days.items(): c.filter_events() - def today_in_calendar(self): return self.firstdate <= self.now and self.lastdate >= self.now @@ -124,53 +127,56 @@ class CalendarList: def fill_calendar_days(self): if self.filter is None: from .models import Event + qs = Event.objects.all() else: qs = self.filter.qs startdatetime = datetime.combine(self.c_firstdate, time.min) lastdatetime = datetime.combine(self.c_lastdate, time.max) self.events = qs.filter( - (Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime)) | - (Q(recurrence_dtend__isnull=False) & ~(Q(recurrence_dtstart__gt=lastdatetime) | Q(recurrence_dtend__lt=startdatetime))) + (Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime)) + | ( + Q(recurrence_dtend__isnull=False) + & ~( + Q(recurrence_dtstart__gt=lastdatetime) + | Q(recurrence_dtend__lt=startdatetime) + ) + ) ).order_by("start_time") firstdate = datetime.fromordinal(self.c_firstdate.toordinal()) if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None: firstdate = timezone.make_aware(firstdate, timezone.get_default_timezone()) - lastdate = datetime.fromordinal(self.c_lastdate.toordinal()) if lastdate.tzinfo is None or lastdate.tzinfo.utcoffset(lastdate) is None: lastdate = timezone.make_aware(lastdate, timezone.get_default_timezone()) - for e in self.events: for e_rec in e.get_recurrences_between(firstdate, lastdate): for d in daterange(e_rec.start_day, e_rec.end_day): if d.__str__() in self.calendar_days: self.calendar_days[d.__str__()].add_event(e_rec) - def create_calendar_days(self): # create daylist self.calendar_days = {} for d in daterange(self.c_firstdate, self.c_lastdate): - self.calendar_days[d.strftime("%Y-%m-%d")] = DayInCalendar(d, d >= self.firstdate and d <= self.lastdate) - + self.calendar_days[d.strftime("%Y-%m-%d")] = DayInCalendar( + d, d >= self.firstdate and d <= self.lastdate + ) def is_single_week(self): return hasattr(self, "week") - def is_full_month(self): return hasattr(self, "month") - def calendar_days_list(self): return list(self.calendar_days.values()) -class CalendarMonth(CalendarList): +class CalendarMonth(CalendarList): def __init__(self, year, month, filter): self.year = year self.month = month @@ -192,7 +198,6 @@ class CalendarMonth(CalendarList): class CalendarWeek(CalendarList): - def __init__(self, year, week, filter): self.year = year self.week = week @@ -210,9 +215,8 @@ class CalendarWeek(CalendarList): class CalendarDay(CalendarList): - def __init__(self, date, filter=None): super().__init__(date, date, filter, exact=True) def get_events(self): - return self.calendar_days_list()[0].events \ No newline at end of file + return self.calendar_days_list()[0].events diff --git a/src/agenda_culturel/celery.py b/src/agenda_culturel/celery.py index 8c68cd4..b92b927 100644 --- a/src/agenda_culturel/celery.py +++ b/src/agenda_culturel/celery.py @@ -12,8 +12,6 @@ from .import_tasks.extractor_ical import * from .import_tasks.custom_extractors import * - - # Set the default Django settings module for the 'celery' program. APP_ENV = os.getenv("APP_ENV", "dev") os.environ.setdefault("DJANGO_SETTINGS_MODULE", f"agenda_culturel.settings.{APP_ENV}") @@ -32,11 +30,14 @@ app.config_from_object("django.conf:settings", namespace="CELERY") # Load task modules from all registered Django apps. app.autodiscover_tasks() + def close_import_task(taskid, success, error_message, importer): from agenda_culturel.models import BatchImportation task = BatchImportation.objects.get(celery_id=taskid) - task.status = BatchImportation.STATUS.SUCCESS if success else BatchImportation.STATUS.FAILED + task.status = ( + BatchImportation.STATUS.SUCCESS if success else BatchImportation.STATUS.FAILED + ) task.nb_initial = importer.get_nb_events() task.nb_imported = importer.get_nb_imported_events() task.nb_updated = importer.get_nb_updated_events() @@ -59,12 +60,11 @@ def import_events_from_json(self, json): # save batch importation importation.save() - logger.info("Import events from json: {}".format(self.request.id)) importer = DBImporterEvents(self.request.id) - #try: + # try: success, error_message = importer.import_events(json) # finally, close task @@ -82,7 +82,7 @@ def run_recurrent_import(self, pk): logger.info("Run recurrent import: {}".format(self.request.id)) - # get the recurrent import + # get the recurrent import rimport = RecurrentImport.objects.get(pk=pk) # create a batch importation @@ -94,7 +94,11 @@ def run_recurrent_import(self, pk): importer = DBImporterEvents(self.request.id) # prepare downloading and extracting processes - downloader = SimpleDownloader() if rimport.downloader == RecurrentImport.DOWNLOADER.SIMPLE else ChromiumHeadlessDownloader() + downloader = ( + SimpleDownloader() + if rimport.downloader == RecurrentImport.DOWNLOADER.SIMPLE + else ChromiumHeadlessDownloader() + ) if rimport.processor == RecurrentImport.PROCESSOR.ICAL: extractor = ICALExtractor() elif rimport.processor == RecurrentImport.PROCESSOR.ICALNOBUSY: @@ -127,7 +131,12 @@ def run_recurrent_import(self, pk): try: # get events from website - events = u2e.process(url, browsable_url, default_values = {"category": category, "location": location, "tags": tags}, published = published) + events = u2e.process( + url, + browsable_url, + default_values={"category": category, "location": location, "tags": tags}, + published=published, + ) # convert it to json json_events = json.dumps(events, default=str) @@ -145,15 +154,20 @@ def run_recurrent_import(self, pk): @app.task(bind=True) def daily_imports(self): from agenda_culturel.models import RecurrentImport + logger.info("Imports quotidiens") - imports = RecurrentImport.objects.filter(recurrence=RecurrentImport.RECURRENCE.DAILY) + imports = RecurrentImport.objects.filter( + recurrence=RecurrentImport.RECURRENCE.DAILY + ) for imp in imports: run_recurrent_import.delay(imp.pk) + @app.task(bind=True) def run_all_recurrent_imports(self): from agenda_culturel.models import RecurrentImport + logger.info("Imports complets") imports = RecurrentImport.objects.all() @@ -164,12 +178,16 @@ def run_all_recurrent_imports(self): @app.task(bind=True) def weekly_imports(self): from agenda_culturel.models import RecurrentImport + logger.info("Imports hebdomadaires") - imports = RecurrentImport.objects.filter(recurrence=RecurrentImport.RECURRENCE.WEEKLY) + imports = RecurrentImport.objects.filter( + recurrence=RecurrentImport.RECURRENCE.WEEKLY + ) for imp in imports: run_recurrent_import.delay(imp.pk) + app.conf.beat_schedule = { "daily_imports": { "task": "agenda_culturel.celery.daily_imports", @@ -179,9 +197,8 @@ app.conf.beat_schedule = { "weekly_imports": { "task": "agenda_culturel.celery.weekly_imports", # Daily imports on Mondays at 2:22 a.m. - "schedule": crontab(hour=2, minute=22, day_of_week='mon'), + "schedule": crontab(hour=2, minute=22, day_of_week="mon"), }, } app.conf.timezone = "Europe/Paris" - diff --git a/src/agenda_culturel/db_importer.py b/src/agenda_culturel/db_importer.py index ae93035..6c513d4 100644 --- a/src/agenda_culturel/db_importer.py +++ b/src/agenda_culturel/db_importer.py @@ -4,11 +4,11 @@ from datetime import datetime from django.utils import timezone import logging + logger = logging.getLogger(__name__) class DBImporterEvents: - def __init__(self, celery_id): self.celery_id = celery_id self.error_message = "" @@ -60,7 +60,7 @@ class DBImporterEvents: # get events for event in structure["events"]: # only process events if they are today or the days after - + if self.event_takes_place_today_or_after(event): # set a default "last modified date" if "last_modified" not in event and self.date is not None: @@ -69,7 +69,7 @@ class DBImporterEvents: # conversion to Event, and return an error if it failed if not self.load_event(event): return (False, self.error_message) - + # finally save the loaded events in database self.save_imported() @@ -92,27 +92,31 @@ class DBImporterEvents: return event["end_day"] >= self.today def save_imported(self): - self.db_event_objects, self.nb_updated, self.nb_removed = Event.import_events(self.event_objects, remove_missing_from_source=self.url) + self.db_event_objects, self.nb_updated, self.nb_removed = Event.import_events( + self.event_objects, remove_missing_from_source=self.url + ) - def is_valid_event_structure(self, event): if "title" not in event: - self.error_message = "JSON is not correctly structured: one event without title" + self.error_message = ( + "JSON is not correctly structured: one event without title" + ) return False if "start_day" not in event: - self.error_message = "JSON is not correctly structured: one event without start_day" + self.error_message = ( + "JSON is not correctly structured: one event without start_day" + ) return False return True - def load_event(self, event): if self.is_valid_event_structure(event): - logger.warning("Valid event: {} {}".format(event["last_modified"], event["title"])) + logger.warning( + "Valid event: {} {}".format(event["last_modified"], event["title"]) + ) event_obj = Event.from_structure(event, self.url) self.event_objects.append(event_obj) return True else: logger.warning("Not valid event: {}".format(event)) return False - - diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms.py index d576147..9fd9f27 100644 --- a/src/agenda_culturel/forms.py +++ b/src/agenda_culturel/forms.py @@ -1,8 +1,31 @@ -from django.forms import ModelForm, ValidationError, TextInput, Form, URLField, MultipleHiddenInput, Textarea, CharField, ChoiceField, RadioSelect, MultipleChoiceField, BooleanField, HiddenInput, ModelChoiceField +from django.forms import ( + ModelForm, + ValidationError, + TextInput, + Form, + URLField, + MultipleHiddenInput, + Textarea, + CharField, + ChoiceField, + RadioSelect, + MultipleChoiceField, + BooleanField, + HiddenInput, + ModelChoiceField, +) from datetime import date from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget -from .models import Event, BatchImportation, RecurrentImport, CategorisationRule, ModerationAnswer, ModerationQuestion, Place +from .models import ( + Event, + BatchImportation, + RecurrentImport, + CategorisationRule, + ModerationAnswer, + ModerationQuestion, + Place, +) from django.utils.translation import gettext_lazy as _ from string import ascii_uppercase as auc from .templatetags.utils_extra import int_to_abc @@ -12,6 +35,7 @@ from django.utils.formats import localize from .templatetags.event_extra import event_field_verbose_name, field_to_html import logging + logger = logging.getLogger(__name__) @@ -22,45 +46,63 @@ class EventSubmissionForm(Form): class DynamicArrayWidgetURLs(DynamicArrayWidget): template_name = "agenda_culturel/widgets/widget-urls.html" + class DynamicArrayWidgetTags(DynamicArrayWidget): template_name = "agenda_culturel/widgets/widget-tags.html" + class RecurrentImportForm(ModelForm): class Meta: model = RecurrentImport - fields = '__all__' + fields = "__all__" widgets = { - 'defaultTags': DynamicArrayWidgetTags(), + "defaultTags": DynamicArrayWidgetTags(), } + class CategorisationRuleImportForm(ModelForm): class Meta: model = CategorisationRule - fields = '__all__' + fields = "__all__" + class EventForm(ModelForm): - class Meta: model = Event - exclude = ["possibly_duplicated", "imported_date", "modified_date", "moderated_date"] + exclude = [ + "possibly_duplicated", + "imported_date", + "modified_date", + "moderated_date", + ] widgets = { - 'start_day': TextInput(attrs={'type': 'date', 'onchange': 'update_datetimes(event);', "onfocus": "this.oldvalue = this.value;"}), - 'start_time': TextInput(attrs={'type': 'time', 'onchange': 'update_datetimes(event);', "onfocus": "this.oldvalue = this.value;"}), - 'end_day': TextInput(attrs={'type': 'date'}), - 'end_time': TextInput(attrs={'type': 'time'}), - 'uuids': MultipleHiddenInput(), - 'import_sources': MultipleHiddenInput(), - 'reference_urls': DynamicArrayWidgetURLs(), - 'tags': DynamicArrayWidgetTags(), + "start_day": TextInput( + attrs={ + "type": "date", + "onchange": "update_datetimes(event);", + "onfocus": "this.oldvalue = this.value;", + } + ), + "start_time": TextInput( + attrs={ + "type": "time", + "onchange": "update_datetimes(event);", + "onfocus": "this.oldvalue = this.value;", + } + ), + "end_day": TextInput(attrs={"type": "date"}), + "end_time": TextInput(attrs={"type": "time"}), + "uuids": MultipleHiddenInput(), + "import_sources": MultipleHiddenInput(), + "reference_urls": DynamicArrayWidgetURLs(), + "tags": DynamicArrayWidgetTags(), } - def __init__(self, *args, **kwargs): - is_authenticated = kwargs.pop('is_authenticated', False) + is_authenticated = kwargs.pop("is_authenticated", False) super().__init__(*args, **kwargs) if not is_authenticated: - del self.fields['status'] - + del self.fields["status"] def clean_end_day(self): start_day = self.cleaned_data.get("start_day") @@ -82,40 +124,71 @@ class EventForm(ModelForm): # both start and end time are defined if start_time is not None and end_time is not None: if start_time > end_time: - raise ValidationError(_("The end time cannot be earlier than the start time.")) + raise ValidationError( + _("The end time cannot be earlier than the start time.") + ) return end_time - class BatchImportationForm(Form): - json = CharField(label="JSON", widget=Textarea(attrs={"rows":"10"}), help_text=_("JSON in the format expected for the import."), required=True) + json = CharField( + label="JSON", + widget=Textarea(attrs={"rows": "10"}), + help_text=_("JSON in the format expected for the import."), + required=True, + ) class FixDuplicates(Form): - - action = ChoiceField() def __init__(self, *args, **kwargs): - nb_events = kwargs.pop('nb_events', None) + nb_events = kwargs.pop("nb_events", None) super().__init__(*args, **kwargs) if nb_events == 2: choices = [("NotDuplicates", "Ces événements sont différents")] - choices += [("SelectA", "Ces événements sont identiques, on garde A et on met B à la corbeille")] - choices += [("SelectB", "Ces événements sont identiques, on garde B et on met A à la corbeille")] - choices += [("Merge", "Ces événements sont identiques, on fusionne à la main")] + choices += [ + ( + "SelectA", + "Ces événements sont identiques, on garde A et on met B à la corbeille", + ) + ] + choices += [ + ( + "SelectB", + "Ces événements sont identiques, on garde B et on met A à la corbeille", + ) + ] + choices += [ + ("Merge", "Ces événements sont identiques, on fusionne à la main") + ] else: choices = [("NotDuplicates", "Ces événements sont tous différents")] for i in auc[0:nb_events]: - choices += [("Remove" + i, "L'événement " + i + " n'est pas identique aux autres, on le rend indépendant")] + choices += [ + ( + "Remove" + i, + "L'événement " + + i + + " n'est pas identique aux autres, on le rend indépendant", + ) + ] for i in auc[0:nb_events]: - choices += [("Select" + i, "Ces événements sont identiques, on garde " + i + " et on met les autres à la corbeille")] - choices += [("Merge", "Ces événements sont identiques, on fusionne à la main")] + choices += [ + ( + "Select" + i, + "Ces événements sont identiques, on garde " + + i + + " et on met les autres à la corbeille", + ) + ] + choices += [ + ("Merge", "Ces événements sont identiques, on fusionne à la main") + ] - - self.fields['action'].choices = choices + self.fields["action"].choices = choices def is_action_no_duplicates(self): return self.cleaned_data["action"] == "NotDuplicates" @@ -145,26 +218,28 @@ class FixDuplicates(Form): class SelectEventInList(Form): - event = ChoiceField() def __init__(self, *args, **kwargs): - events = kwargs.pop('events', None) + events = kwargs.pop("events", None) super().__init__(*args, **kwargs) - self.fields['event'].choices = [(e.pk, str(e.start_day) + " " + e.title + ", " + e.location) for e in events] + self.fields["event"].choices = [ + (e.pk, str(e.start_day) + " " + e.title + ", " + e.location) for e in events + ] class MergeDuplicates(Form): - checkboxes_fields = ["reference_urls", "description"] def __init__(self, *args, **kwargs): - self.duplicates = kwargs.pop('duplicates', None) + self.duplicates = kwargs.pop("duplicates", None) nb_events = self.duplicates.nb_duplicated() super().__init__(*args, **kwargs) - choices = [("event" + i, "Valeur de l'évenement " + i) for i in auc[0:nb_events]] + choices = [ + ("event" + i, "Valeur de l'évenement " + i) for i in auc[0:nb_events] + ] for f in self.duplicates.get_items_comparison(): if not f["similar"]: @@ -172,42 +247,61 @@ class MergeDuplicates(Form): self.fields[f["key"]] = MultipleChoiceField(choices=choices) self.fields[f["key"]].initial = choices[0][0] else: - self.fields[f["key"]] = ChoiceField(widget=RadioSelect, choices=choices) + self.fields[f["key"]] = ChoiceField( + widget=RadioSelect, choices=choices + ) self.fields[f["key"]].initial = choices[0][0] - def as_grid(self): result = '
' for i, e in enumerate(self.duplicates.get_duplicated()): result += '
' - result += '
' + int_to_abc(i) + '
' - result += '
    ' - result += '
  • ' + e.title + '
  • ' - result += '
  • Création : ' + localize(localtime(e.created_date)) + '
  • ' - result += '
  • Dernière modification : ' + localize(localtime(e.modified_date)) + '
  • ' + result += '
    ' + int_to_abc(i) + "
    " + result += "
      " + result += ( + '
    • ' + e.title + "
    • " + ) + result += ( + "
    • Création : " + localize(localtime(e.created_date)) + "
    • " + ) + result += ( + "
    • Dernière modification : " + + localize(localtime(e.modified_date)) + + "
    • " + ) if e.imported_date: - result += '
    • Dernière importation : ' + localize(localtime(e.imported_date)) + '
    • ' - result += '
    ' - result += '
' - result += '
' + result += ( + "
  • Dernière importation : " + + localize(localtime(e.imported_date)) + + "
  • " + ) + result += "" + result += "" + result += "" for e in self.duplicates.get_items_comparison(): key = e["key"] result += "

    " + event_field_verbose_name(e["key"]) + "

    " if e["similar"]: - result += '
    Identique :' + str(field_to_html(e["values"], e["key"])) + '
    ' + result += ( + '
    Identique :' + + str(field_to_html(e["values"], e["key"])) + + "
    " + ) else: - result += '
    ' + result += "
    " result += '
    ' if hasattr(self, "cleaned_data"): checked = self.cleaned_data.get(key) else: checked = self.fields[key].initial - for i, (v, radio) in enumerate(zip(e["values"], self.fields[e["key"]].choices)): + for i, (v, radio) in enumerate( + zip(e["values"], self.fields[e["key"]].choices) + ): result += '
    ' - id = 'id_' + key + '_' + str(i) - value = 'event' + auc[i] + id = "id_" + key + "_" + str(i) + value = "event" + auc[i] result += '' + str(field_to_html(v, e["key"])) + '
    ' + result += ">" + result += ( + '
    ' + + int_to_abc(i) + + "
    " + + str(field_to_html(v, e["key"])) + + "
    " + ) result += "
    " return mark_safe(result) - def get_selected_events_id(self, key): value = self.cleaned_data.get(key) if not key in self.fields: @@ -240,20 +339,20 @@ class MergeDuplicates(Form): class ModerationQuestionForm(ModelForm): class Meta: model = ModerationQuestion - fields = '__all__' + fields = "__all__" + class ModerationAnswerForm(ModelForm): class Meta: model = ModerationAnswer - exclude = ['question'] + exclude = ["question"] widgets = { - 'adds_tags': DynamicArrayWidgetTags(), - 'removes_tags': DynamicArrayWidgetTags() + "adds_tags": DynamicArrayWidgetTags(), + "removes_tags": DynamicArrayWidgetTags(), } class ModerateForm(ModelForm): - class Meta: model = Event fields = [] @@ -265,75 +364,104 @@ class ModerateForm(ModelForm): mas = ModerationAnswer.objects.all() for q in mqs: - self.fields[q.complete_id()] = ChoiceField(widget=RadioSelect, label=q.question, choices=[(a.pk, a.html_description()) for a in mas if a.question == q], required=True) + self.fields[q.complete_id()] = ChoiceField( + widget=RadioSelect, + label=q.question, + choices=[(a.pk, a.html_description()) for a in mas if a.question == q], + required=True, + ) for a in mas: if a.question == q and a.valid_event(self.instance): self.fields[q.complete_id()].initial = a.pk break -class CategorisationForm(Form): +class CategorisationForm(Form): def __init__(self, *args, **kwargs): if "events" in kwargs: - events = kwargs.pop('events', None) + events = kwargs.pop("events", None) else: events = [] for f in args[0]: - logger.warning('fff: ' + f) - if '_' not in f: - if f + '_cat' in args[0]: - events.append((Event.objects.get(pk=int(f)), args[0][f + '_cat'])) + logger.warning("fff: " + f) + if "_" not in f: + if f + "_cat" in args[0]: + events.append( + (Event.objects.get(pk=int(f)), args[0][f + "_cat"]) + ) super().__init__(*args, **kwargs) for e, c in events: - self.fields[str(e.pk)] = BooleanField(initial=False, label=_('Apply category {} to the event {}').format(c, e.title), required=False) + self.fields[str(e.pk)] = BooleanField( + initial=False, + label=_("Apply category {} to the event {}").format(c, e.title), + required=False, + ) self.fields[str(e.pk) + "_cat"] = CharField(initial=c, widget=HiddenInput()) def get_validated(self): - return [(e, self.cleaned_data.get(e + '_cat')) for e in self.fields if '_' not in e and self.cleaned_data.get(e)] + return [ + (e, self.cleaned_data.get(e + "_cat")) + for e in self.fields + if "_" not in e and self.cleaned_data.get(e) + ] class EventAddPlaceForm(Form): - - place = ModelChoiceField(label=_("Place"), queryset=Place.objects.all().order_by("name"), empty_label=_("Create a missing place"), required=False) + place = ModelChoiceField( + label=_("Place"), + queryset=Place.objects.all().order_by("name"), + empty_label=_("Create a missing place"), + required=False, + ) add_alias = BooleanField(initial=True, required=False) def __init__(self, *args, **kwargs): - self.instance = kwargs.pop('instance', False) + self.instance = kwargs.pop("instance", False) super().__init__(*args, **kwargs) if self.instance.location: - self.fields["add_alias"].label = _("Add \"{}\" to the aliases of the place").format(self.instance.location) + self.fields["add_alias"].label = _( + 'Add "{}" to the aliases of the place' + ).format(self.instance.location) else: self.fields.pop("add_alias") def modified_event(self): - return self.cleaned_data.get('place') + return self.cleaned_data.get("place") def save(self): if self.cleaned_data.get("place"): place = self.cleaned_data.get("place") self.instance.exact_location = place self.instance.save() - if self.cleaned_data.get('add_alias'): + if self.cleaned_data.get("add_alias"): place.aliases.append(self.instance.location) place.save() - + return self.instance + class PlaceForm(ModelForm): - apply_to_all = BooleanField(initial=True, label=_('On saving, use aliases to detect all matching events with missing place'), required=False) + apply_to_all = BooleanField( + initial=True, + label=_( + "On saving, use aliases to detect all matching events with missing place" + ), + required=False, + ) class Meta: model = Place - fields = '__all__' - widgets = { - 'location': TextInput() - } - - def as_grid(self): - return mark_safe('
    ' + super().as_p() + '
    ' + - '

    Cliquez pour ajuster la position GPS

    ') + fields = "__all__" + widgets = {"location": TextInput()} + def as_grid(self): + return mark_safe( + '
    ' + + super().as_p() + + '
    ' + + '

    Cliquez pour ajuster la position GPS

    ' + ) def apply(self): - return self.cleaned_data.get("apply_to_all") + return self.cleaned_data.get("apply_to_all") diff --git a/src/agenda_culturel/import_tasks/custom_extractors/__init__.py b/src/agenda_culturel/import_tasks/custom_extractors/__init__.py index 1cb91d4..ecf5ec1 100644 --- a/src/agenda_culturel/import_tasks/custom_extractors/__init__.py +++ b/src/agenda_culturel/import_tasks/custom_extractors/__init__.py @@ -1,4 +1,7 @@ from os.path import dirname, basename, isfile, join import glob + modules = glob.glob(join(dirname(__file__), "*.py")) -__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] \ No newline at end of file +__all__ = [ + basename(f)[:-3] for f in modules if isfile(f) and not f.endswith("__init__.py") +] diff --git a/src/agenda_culturel/import_tasks/custom_extractors/lacomedie.py b/src/agenda_culturel/import_tasks/custom_extractors/lacomedie.py index fcb0664..a225adc 100644 --- a/src/agenda_culturel/import_tasks/custom_extractors/lacomedie.py +++ b/src/agenda_culturel/import_tasks/custom_extractors/lacomedie.py @@ -8,25 +8,30 @@ from datetime import timedelta # URL: https://lacomediedeclermont.com/saison23-24/wp-admin/admin-ajax.php?action=load_dates_existantes # URL pour les humains: https://lacomediedeclermont.com/saison23-24/ class CExtractor(TwoStepsExtractor): - nom_lieu = "La Comédie de Clermont" def category_comedie2agenda(self, category): - mapping = { "Théâtre": "Théâtre", "Danse": "Danse", "Rencontre": "Autre", "Sortie de résidence": "Autre", "PopCorn Live": "Autre"} + mapping = { + "Théâtre": "Théâtre", + "Danse": "Danse", + "Rencontre": "Autre", + "Sortie de résidence": "Autre", + "PopCorn Live": "Autre", + } if category in mapping: return mapping[category] else: return None - - def build_event_url_list(self, content): dates = json5.loads(content)["data"][0] - + url = self.url.split("?")[0] for d in list(set(dates)): if not self.only_future or self.now <= datetime.date.fromisoformat(d): - events = self.downloader.get_content(url, post={'action': "load_evenements_jour", "jour": d}) + events = self.downloader.get_content( + url, post={"action": "load_evenements_jour", "jour": d} + ) if events: events = json5.loads(events) if "data" in events: @@ -34,27 +39,43 @@ class CExtractor(TwoStepsExtractor): soup = BeautifulSoup(events, "html.parser") events = soup.select("div.unedatedev") for e in events: - e_url = e.select('a')[0]["href"] + "#" + d # a "fake" url specific for each day of this show + e_url = ( + e.select("a")[0]["href"] + "#" + d + ) # a "fake" url specific for each day of this show self.add_event_url(e_url) self.add_event_start_day(e_url, d) - t = str(e.select('div#datecal')[0]).split(' ')[-1].split('<')[0] + t = ( + str(e.select("div#datecal")[0]) + .split(" ")[-1] + .split("<")[0] + ) self.add_event_start_time(e_url, t) - title = e.select('a')[0].contents[0] + title = e.select("a")[0].contents[0] self.add_event_title(e_url, title) category = e.select("div#lieuevtcal span") if len(category) > 0: - category = self.category_comedie2agenda(category[-1].contents[0]) + category = self.category_comedie2agenda( + category[-1].contents[0] + ) if category is not None: self.add_event_category(e_url, category) - location = e.select("div#lieuevtcal")[0].contents[-1].split("•")[-1] + location = ( + e.select("div#lieuevtcal")[0] + .contents[-1] + .split("•")[-1] + ) self.add_event_location(e_url, location) - - - - def add_event_from_content(self, event_content, event_url, url_human = None, default_values = None, published = False): + def add_event_from_content( + self, + event_content, + event_url, + url_human=None, + default_values=None, + published=False, + ): soup = BeautifulSoup(event_content, "html.parser") - + image = soup.select("#imgspec img") if image: image = image[0]["src"] @@ -65,5 +86,17 @@ class CExtractor(TwoStepsExtractor): url_human = event_url - self.add_event_with_props(event_url, None, None, None, None, description, [], recurrences=None, uuids=[event_url], url_human=url_human, published=published, image=image) - + self.add_event_with_props( + event_url, + None, + None, + None, + None, + description, + [], + recurrences=None, + uuids=[event_url], + url_human=url_human, + published=published, + image=image, + ) diff --git a/src/agenda_culturel/import_tasks/custom_extractors/lacoope.py b/src/agenda_culturel/import_tasks/custom_extractors/lacoope.py index 363c355..d54a965 100644 --- a/src/agenda_culturel/import_tasks/custom_extractors/lacoope.py +++ b/src/agenda_culturel/import_tasks/custom_extractors/lacoope.py @@ -3,31 +3,39 @@ import re import json5 from datetime import timedelta + # A class dedicated to get events from La Coopérative de Mai: # URL: https://www.lacoope.org/concerts-calendrier/ class CExtractor(TwoStepsExtractor): - nom_lieu = "La Coopérative de Mai" def build_event_url_list(self, content): soup = BeautifulSoup(content, "html.parser") - script = soup.find('div', class_="js-filter__results").findChildren('script') + script = soup.find("div", class_="js-filter__results").findChildren("script") if len(script) == 0: raise Exception("Cannot find events in the first page") script = script[0] - search = re.search(r"window.fullCalendarContent = (.*)", str(script), re.S) + search = re.search( + r"window.fullCalendarContent = (.*)", str(script), re.S + ) if search: data = json5.loads(search.group(1)) - for e in data['events']: - self.add_event_url(e['url']) - if e['tag'] == "Gratuit": - self.add_event_tag(e['url'], 'gratuit') + for e in data["events"]: + self.add_event_url(e["url"]) + if e["tag"] == "Gratuit": + self.add_event_tag(e["url"], "gratuit") else: - raise Exception('Cannot extract events from javascript') + raise Exception("Cannot extract events from javascript") - - def add_event_from_content(self, event_content, event_url, url_human = None, default_values = None, published = False): + def add_event_from_content( + self, + event_content, + event_url, + url_human=None, + default_values=None, + published=False, + ): soup = BeautifulSoup(event_content, "html.parser") title = soup.find("h1").contents[0] @@ -38,9 +46,9 @@ class CExtractor(TwoStepsExtractor): description = soup.find("div", class_="grid-concert-content") if description: - description = description.find('div', class_="content-striped") + description = description.find("div", class_="content-striped") if description: - description = description.find('div', class_='wysiwyg') + description = description.find("div", class_="wysiwyg") if description: description = description.get_text() if description is None: @@ -50,8 +58,8 @@ class CExtractor(TwoStepsExtractor): link_calendar = soup.select('a[href^="https://calendar.google.com/calendar/"]') if len(link_calendar) == 0: - raise Exception('Cannot find the google calendar url') - + raise Exception("Cannot find the google calendar url") + gg_cal = GGCalendar(link_calendar[0]["href"]) start_day = gg_cal.start_day start_time = gg_cal.start_time @@ -60,5 +68,20 @@ class CExtractor(TwoStepsExtractor): location = CExtractor.nom_lieu url_human = event_url - self.add_event_with_props(event_url, title, category, start_day, location, description, tags, recurrences=None, uuids=[event_url], url_human=url_human, start_time=start_time, end_day=end_day, end_time=end_time, published=published, image=image) - + self.add_event_with_props( + event_url, + title, + category, + start_day, + location, + description, + tags, + recurrences=None, + uuids=[event_url], + url_human=url_human, + start_time=start_time, + end_day=end_day, + end_time=end_time, + published=published, + image=image, + ) diff --git a/src/agenda_culturel/import_tasks/custom_extractors/lapucealoreille.py b/src/agenda_culturel/import_tasks/custom_extractors/lapucealoreille.py index 9a978d1..3ea6eb0 100644 --- a/src/agenda_culturel/import_tasks/custom_extractors/lapucealoreille.py +++ b/src/agenda_culturel/import_tasks/custom_extractors/lapucealoreille.py @@ -7,7 +7,6 @@ from datetime import timedelta # A class dedicated to get events from La puce à l'oreille # URL: https://www.lapucealoreille63.fr/ class CExtractor(TwoStepsExtractor): - nom_lieu = "La Puce à l'Oreille" def build_event_url_list(self, content): @@ -24,11 +23,19 @@ class CExtractor(TwoStepsExtractor): title = re.sub(" +", " ", title) self.add_event_title(e_url["href"], title) - - def add_event_from_content(self, event_content, event_url, url_human = None, default_values = None, published = False): + def add_event_from_content( + self, + event_content, + event_url, + url_human=None, + default_values=None, + published=False, + ): soup = BeautifulSoup(event_content, "html.parser") - start_day = self.parse_french_date(soup.find("h2").get_text()) # pas parfait, mais bordel que ce site est mal construit + start_day = self.parse_french_date( + soup.find("h2").get_text() + ) # pas parfait, mais bordel que ce site est mal construit spans = soup.select("div[data-testid=richTextElement] span") start_time = None @@ -62,12 +69,30 @@ class CExtractor(TwoStepsExtractor): image = image[0]["src"] else: image = None - - descriptions = soup.select("div[data-testid=mesh-container-content] div[data-testid=inline-content] div[data-testid=mesh-container-content] div[data-testid=richTextElement]") + + descriptions = soup.select( + "div[data-testid=mesh-container-content] div[data-testid=inline-content] div[data-testid=mesh-container-content] div[data-testid=richTextElement]" + ) if descriptions: descriptions = [d.get_text() for d in descriptions] description = max(descriptions, key=len) else: description = None - self.add_event_with_props(event_url, None, "Concert", start_day, location, description, tags, recurrences=None, uuids=[event_url], url_human=url_human, start_time=start_time, end_day=end_day, end_time=end_time, published=published, image=image) + self.add_event_with_props( + event_url, + None, + "Concert", + start_day, + location, + description, + tags, + recurrences=None, + uuids=[event_url], + url_human=url_human, + start_time=start_time, + end_day=end_day, + end_time=end_time, + published=published, + image=image, + ) diff --git a/src/agenda_culturel/import_tasks/custom_extractors/lefotomat.py b/src/agenda_culturel/import_tasks/custom_extractors/lefotomat.py index dff57f7..90fc8fc 100644 --- a/src/agenda_culturel/import_tasks/custom_extractors/lefotomat.py +++ b/src/agenda_culturel/import_tasks/custom_extractors/lefotomat.py @@ -7,19 +7,17 @@ from datetime import timedelta # A class dedicated to get events from Le Fotomat' # URL: https://www.lefotomat.com/ class CExtractor(TwoStepsExtractor): - nom_lieu = "Le Fotomat'" def category_fotomat2agenda(self, category): if not category: return None - mapping = { "Concerts": "Concert"} + mapping = {"Concerts": "Concert"} if category in mapping: return mapping[category] else: return None - def build_event_url_list(self, content): soup = BeautifulSoup(content, "xml") @@ -34,10 +32,15 @@ class CExtractor(TwoStepsExtractor): category = self.category_fotomat2agenda(e.find("category").contents[0]) if category: self.add_event_category(e_url, category) - - - def add_event_from_content(self, event_content, event_url, url_human = None, default_values = None, published = False): + def add_event_from_content( + self, + event_content, + event_url, + url_human=None, + default_values=None, + published=False, + ): soup = BeautifulSoup(event_content, "html.parser") image = soup.select("div.post-content img.wp-post-image") if image: @@ -62,11 +65,26 @@ class CExtractor(TwoStepsExtractor): tags = [] for c in article[0]["class"]: if c.startswith("category-"): - tag = '-'.join(c.split("-")[1:]) + tag = "-".join(c.split("-")[1:]) if tag != "concerts": tags.append(tag) url_human = event_url - self.add_event_with_props(event_url, None, None, start_day, location, description, tags, recurrences=None, uuids=[event_url], url_human=url_human, start_time=start_time, end_day=end_day, end_time=end_time, published=published, image=image) - + self.add_event_with_props( + event_url, + None, + None, + start_day, + location, + description, + tags, + recurrences=None, + uuids=[event_url], + url_human=url_human, + start_time=start_time, + end_day=end_day, + end_time=end_time, + published=published, + image=image, + ) diff --git a/src/agenda_culturel/import_tasks/downloader.py b/src/agenda_culturel/import_tasks/downloader.py index 785e4f3..c88c7a6 100644 --- a/src/agenda_culturel/import_tasks/downloader.py +++ b/src/agenda_culturel/import_tasks/downloader.py @@ -8,7 +8,6 @@ from abc import ABC, abstractmethod class Downloader(ABC): - def __init__(self): pass @@ -16,7 +15,7 @@ class Downloader(ABC): def download(self, url, post=None): pass - def get_content(self, url, cache = None, post = None): + def get_content(self, url, cache=None, post=None): if cache and os.path.exists(cache): print("Loading cache ({})".format(cache)) with open(cache) as f: @@ -35,11 +34,9 @@ class Downloader(ABC): class SimpleDownloader(Downloader): - def __init__(self): super().__init__() - def download(self, url, post=None): print("Downloading {}".format(url)) @@ -56,9 +53,7 @@ class SimpleDownloader(Downloader): return None - class ChromiumHeadlessDownloader(Downloader): - def __init__(self): super().__init__() self.options = Options() @@ -67,10 +62,9 @@ class ChromiumHeadlessDownloader(Downloader): self.options.add_argument("--no-sandbox") self.service = Service("/usr/bin/chromedriver") - def download(self, url, post=None): if post: - raise Exception('POST method with Chromium headless not yet implemented') + raise Exception("POST method with Chromium headless not yet implemented") print("Download {}".format(url)) self.driver = webdriver.Chrome(service=self.service, options=self.options) diff --git a/src/agenda_culturel/import_tasks/extractor.py b/src/agenda_culturel/import_tasks/extractor.py index eff1feb..66487b5 100644 --- a/src/agenda_culturel/import_tasks/extractor.py +++ b/src/agenda_culturel/import_tasks/extractor.py @@ -6,11 +6,11 @@ import unicodedata def remove_accents(input_str): - nfkd_form = unicodedata.normalize('NFKD', input_str) - return u"".join([c for c in nfkd_form if not unicodedata.combining(c)]) + nfkd_form = unicodedata.normalize("NFKD", input_str) + return "".join([c for c in nfkd_form if not unicodedata.combining(c)]) + class Extractor(ABC): - def __init__(self): self.header = {} self.events = [] @@ -26,7 +26,20 @@ class Extractor(ABC): return start_day def guess_month(self, text): - mths = ["jan", "fe", "mar", "av", "mai", "juin", "juill", "ao", "sep", "oct", "nov", "dec"] + mths = [ + "jan", + "fe", + "mar", + "av", + "mai", + "juin", + "juill", + "ao", + "sep", + "oct", + "nov", + "dec", + ] t = remove_accents(text).lower() for i, m in enumerate(mths): if t.startswith(m): @@ -35,14 +48,16 @@ class Extractor(ABC): def parse_french_date(self, text): # format NomJour Numero Mois Année - m = re.search('[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)', text) + m = re.search( + "[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text + ) if m: day = m.group(1) month = self.guess_month(m.group(2)) year = m.group(3) else: # format Numero Mois Annee - m = re.search('([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)', text) + m = re.search("([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text) if m: day = m.group(1) month = self.guess_month(m.group(2)) @@ -50,7 +65,7 @@ class Extractor(ABC): else: # TODO: consolider les cas non satisfaits return None - + if month is None: return None try: @@ -66,28 +81,28 @@ class Extractor(ABC): def parse_french_time(self, text): # format heures minutes secondes - m = re.search('([0-9]+)[ a-zA-Z:.]+([0-9]+)[ a-zA-Z:.]+([0-9]+)', text) + m = re.search("([0-9]+)[ a-zA-Z:.]+([0-9]+)[ a-zA-Z:.]+([0-9]+)", text) if m: h = m.group(1) m = m.group(2) s = m.group(3) else: # format heures minutes - m = re.search('([0-9]+)[ hH:.]+([0-9]+)', text) + m = re.search("([0-9]+)[ hH:.]+([0-9]+)", text) if m: h = m.group(1) m = m.group(2) s = "0" else: # format heures - m = re.search('([0-9]+)[ Hh:.]', text) + m = re.search("([0-9]+)[ Hh:.]", text) if m: h = m.group(1) m = "0" s = "0" else: return None - + try: h = int(h) m = int(m) @@ -98,10 +113,10 @@ class Extractor(ABC): return None return time(h, m, s) - - @abstractmethod - def extract(self, content, url, url_human = None, default_values = None, published = False): + def extract( + self, content, url, url_human=None, default_values=None, published=False + ): pass def set_downloader(self, downloader): @@ -118,7 +133,25 @@ class Extractor(ABC): def clear_events(self): self.events = [] - def add_event(self, title, category, start_day, location, description, tags, uuids, recurrences=None, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False, image=None, image_alt=None): + def add_event( + self, + title, + category, + start_day, + location, + description, + tags, + uuids, + recurrences=None, + url_human=None, + start_time=None, + end_day=None, + end_time=None, + last_modified=None, + published=False, + image=None, + image_alt=None, + ): if title is None: print("ERROR: cannot import an event without name") return @@ -136,7 +169,7 @@ class Extractor(ABC): "tags": tags, "published": published, "image": image, - "image_alt": image_alt + "image_alt": image_alt, } # TODO: pourquoi url_human et non reference_url if url_human is not None: @@ -157,10 +190,14 @@ class Extractor(ABC): self.events.append(event) def default_value_if_exists(self, default_values, key): - return default_values[key] if default_values is not None and key in default_values else None + return ( + default_values[key] + if default_values is not None and key in default_values + else None + ) def get_structure(self): - return { "header": self.header, "events": self.events} + return {"header": self.header, "events": self.events} def clean_url(url): from .extractor_ical import ICALExtractor @@ -178,4 +215,4 @@ class Extractor(ABC): if single_event: return [FacebookEventExtractor(single_event=True)] else: - return [ICALExtractor(), FacebookEventExtractor(single_event=False)] \ No newline at end of file + return [ICALExtractor(), FacebookEventExtractor(single_event=False)] diff --git a/src/agenda_culturel/import_tasks/extractor_facebook.py b/src/agenda_culturel/import_tasks/extractor_facebook.py index 7ebd44b..983a1a1 100644 --- a/src/agenda_culturel/import_tasks/extractor_facebook.py +++ b/src/agenda_culturel/import_tasks/extractor_facebook.py @@ -9,13 +9,12 @@ from .extractor import * import json import logging + logger = logging.getLogger(__name__) class FacebookEventExtractor(Extractor): - class SimpleFacebookEvent: - def __init__(self, data): self.elements = {} @@ -23,31 +22,40 @@ class FacebookEventExtractor(Extractor): self.elements[key] = data[key] if key in data else None if "parent_event" in data: - self.parent = FacebookEventExtractor.SimpleFacebookEvent(data["parent_event"]) - + self.parent = FacebookEventExtractor.SimpleFacebookEvent( + data["parent_event"] + ) class FacebookEvent: - name = "event" keys = [ - ["start_time_formatted", 'start_timestamp', - 'is_past', - "name", - "price_info", - "cover_media_renderer", - "event_creator", - "id", - "day_time_sentence", - "event_place", - "comet_neighboring_siblings"], - ["event_description"], - ["start_timestamp", "end_timestamp"] + [ + "start_time_formatted", + "start_timestamp", + "is_past", + "name", + "price_info", + "cover_media_renderer", + "event_creator", + "id", + "day_time_sentence", + "event_place", + "comet_neighboring_siblings", + ], + ["event_description"], + ["start_timestamp", "end_timestamp"], ] rules = { - "event_description": { "description": ["text"]}, - "cover_media_renderer": {"image_alt": ["cover_photo", "photo", "accessibility_caption"], "image": ["cover_photo", "photo", "full_image", "uri"]}, - "event_creator": { "event_creator_name": ["name"], "event_creator_url": ["url"] }, - "event_place": {"event_place_name": ["name"] } + "event_description": {"description": ["text"]}, + "cover_media_renderer": { + "image_alt": ["cover_photo", "photo", "accessibility_caption"], + "image": ["cover_photo", "photo", "full_image", "uri"], + }, + "event_creator": { + "event_creator_name": ["name"], + "event_creator_url": ["url"], + }, + "event_place": {"event_place_name": ["name"]}, } def __init__(self, i, event): @@ -60,26 +68,36 @@ class FacebookEventExtractor(Extractor): def get_element(self, key): return self.elements[key] if key in self.elements else None - def get_element_date(self, key): v = self.get_element(key) - return datetime.fromtimestamp(v).date() if v is not None and v != 0 else None - + return ( + datetime.fromtimestamp(v).date() if v is not None and v != 0 else None + ) + def get_element_time(self, key): v = self.get_element(key) - return datetime.fromtimestamp(v).strftime('%H:%M') if v is not None and v != 0 else None + return ( + datetime.fromtimestamp(v).strftime("%H:%M") + if v is not None and v != 0 + else None + ) def add_fragment(self, i, event): self.fragments[i] = event - if FacebookEventExtractor.FacebookEvent.keys[i] == ["start_timestamp", "end_timestamp"]: + if FacebookEventExtractor.FacebookEvent.keys[i] == [ + "start_timestamp", + "end_timestamp", + ]: self.get_possible_end_timestamp(i, event) else: for k in FacebookEventExtractor.FacebookEvent.keys[i]: if k == "comet_neighboring_siblings": self.get_neighbor_events(event[k]) elif k in FacebookEventExtractor.FacebookEvent.rules: - for nk, rule in FacebookEventExtractor.FacebookEvent.rules[k].items(): + for nk, rule in FacebookEventExtractor.FacebookEvent.rules[ + k + ].items(): error = False c = event[k] for ki in rule: @@ -92,83 +110,101 @@ class FacebookEventExtractor(Extractor): else: self.elements[k] = event[k] - def get_possible_end_timestamp(self, i, data): - self.possible_end_timestamp.append(dict((k, data[k]) for k in FacebookEventExtractor.FacebookEvent.keys[i])) - + self.possible_end_timestamp.append( + dict((k, data[k]) for k in FacebookEventExtractor.FacebookEvent.keys[i]) + ) def get_neighbor_events(self, data): - self.neighbor_events = [FacebookEventExtractor.SimpleFacebookEvent(d) for d in data] + self.neighbor_events = [ + FacebookEventExtractor.SimpleFacebookEvent(d) for d in data + ] def __str__(self): - return str(self.elements) + "\n Neighbors: " + ", ".join([ne.elements["id"] for ne in self.neighbor_events]) + return ( + str(self.elements) + + "\n Neighbors: " + + ", ".join([ne.elements["id"] for ne in self.neighbor_events]) + ) def consolidate_current_event(self): - if self.neighbor_events is not None and "id" in self.elements and "end_timestamp" not in self.elements: + if ( + self.neighbor_events is not None + and "id" in self.elements + and "end_timestamp" not in self.elements + ): if self.neighbor_events is not None and "id" in self.elements: id = self.elements["id"] for ne in self.neighbor_events: if ne.elements["id"] == id: - self.elements["end_timestamp"] = ne.elements["end_timestamp"] + self.elements["end_timestamp"] = ne.elements[ + "end_timestamp" + ] - if "end_timestamp" not in self.elements and len(self.possible_end_timestamp) != 0: + if ( + "end_timestamp" not in self.elements + and len(self.possible_end_timestamp) != 0 + ): for s in self.possible_end_timestamp: - if "start_timestamp" in s and "start_timestamp" in self.elements and s["start_timestamp"] == self.elements["start_timestamp"]: + if ( + "start_timestamp" in s + and "start_timestamp" in self.elements + and s["start_timestamp"] == self.elements["start_timestamp"] + ): self.elements["end_timestamp"] = s["end_timestamp"] break - def find_event_fragment_in_array(array, event, first = True): + def find_event_fragment_in_array(array, event, first=True): if isinstance(array, dict): - seen = False for i, ks in enumerate(FacebookEventExtractor.FacebookEvent.keys): if len(ks) == len([k for k in ks if k in array]): seen = True if event is None: - event = FacebookEventExtractor.FacebookEvent(i, array) + event = FacebookEventExtractor.FacebookEvent(i, array) else: event.add_fragment(i, array) # only consider the first of FacebookEvent.keys break if not seen: for k in array: - event = FacebookEventExtractor.FacebookEvent.find_event_fragment_in_array(array[k], event, False) + event = FacebookEventExtractor.FacebookEvent.find_event_fragment_in_array( + array[k], event, False + ) elif isinstance(array, list): for e in array: - event = FacebookEventExtractor.FacebookEvent.find_event_fragment_in_array(e, event, False) + event = FacebookEventExtractor.FacebookEvent.find_event_fragment_in_array( + e, event, False + ) if event is not None and first: event.consolidate_current_event() return event - def build_event(self, url): image = self.get_element("image") return { - "title": self.get_element("name"), - "category": None, - "start_day": self.get_element_date("start_timestamp"), - "location": self.get_element("event_place_name"), - "description": self.get_element("description"), - "tags": [], + "title": self.get_element("name"), + "category": None, + "start_day": self.get_element_date("start_timestamp"), + "location": self.get_element("event_place_name"), + "description": self.get_element("description"), + "tags": [], "uuids": [url], "url_human": url, - "start_time": self.get_element_time("start_timestamp"), - "end_day": self.get_element_date("end_timestamp"), - "end_time": self.get_element_time("end_timestamp"), + "start_time": self.get_element_time("start_timestamp"), + "end_day": self.get_element_date("end_timestamp"), + "end_time": self.get_element_time("end_timestamp"), "image": self.get_element("image"), "image_alt": self.get_element("image"), } - def __init__(self, single_event=False): self.single_event = single_event super().__init__() - def clean_url(url): - if FacebookEventExtractor.is_known_url(url): u = urlparse(url) return "https://www.facebook.com" + u.path @@ -179,17 +215,20 @@ class FacebookEventExtractor(Extractor): u = urlparse(url) return u.netloc in ["facebook.com", "www.facebook.com", "m.facebook.com"] - - def extract(self, content, url, url_human = None, default_values = None, published = False): + def extract( + self, content, url, url_human=None, default_values=None, published=False + ): # NOTE: this method does not use url_human = None and default_values = None # get step by step all information from the content fevent = None soup = BeautifulSoup(content, "html.parser") - for json_script in soup.find_all('script', type="application/json"): + for json_script in soup.find_all("script", type="application/json"): json_txt = json_script.get_text() json_struct = json.loads(json_txt) - fevent = FacebookEventExtractor.FacebookEvent.find_event_fragment_in_array(json_struct, fevent) + fevent = FacebookEventExtractor.FacebookEvent.find_event_fragment_in_array( + json_struct, fevent + ) if fevent is not None: self.set_header(url) @@ -198,5 +237,5 @@ class FacebookEventExtractor(Extractor): event["published"] = published self.add_event(**event) return self.get_structure() - - return None \ No newline at end of file + + return None diff --git a/src/agenda_culturel/import_tasks/extractor_ical.py b/src/agenda_culturel/import_tasks/extractor_ical.py index 7b184d7..94318b2 100644 --- a/src/agenda_culturel/import_tasks/extractor_ical.py +++ b/src/agenda_culturel/import_tasks/extractor_ical.py @@ -14,13 +14,11 @@ from celery.utils.log import get_task_logger logger = get_task_logger(__name__) - class ICALExtractor(Extractor): - def __init__(self): super().__init__() - def get_item_from_vevent(self, event, name, raw = False): + def get_item_from_vevent(self, event, name, raw=False): try: r = event.decoded(name) if raw: @@ -31,7 +29,7 @@ class ICALExtractor(Extractor): return None def get_dt_item_from_vevent(self, event, name): - item = self.get_item_from_vevent(event, name, raw = True) + item = self.get_item_from_vevent(event, name, raw=True) day = None time = None @@ -49,25 +47,26 @@ class ICALExtractor(Extractor): def clean_url(url): return url - - def extract(self, content, url, url_human = None, default_values = None, published = False): + def extract( + self, content, url, url_human=None, default_values=None, published=False + ): warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning) print("Extracting ical events from {}".format(url)) self.set_header(url) self.clear_events() self.uuids = {} - + calendar = icalendar.Calendar.from_ical(content) - for event in calendar.walk('VEVENT'): + for event in calendar.walk("VEVENT"): title = self.get_item_from_vevent(event, "SUMMARY") category = self.default_value_if_exists(default_values, "category") start_day, start_time = self.get_dt_item_from_vevent(event, "DTSTART") end_day, end_time = self.get_dt_item_from_vevent(event, "DTEND") - + # if the start and end are only defined by dates (and not times), # then the event does not occurs on the last day (because it is the end # of the event...) @@ -81,9 +80,9 @@ class ICALExtractor(Extractor): description = self.get_item_from_vevent(event, "DESCRIPTION") if description is not None: soup = BeautifulSoup(description, features="lxml") - delimiter = '\n' - for line_break in soup.findAll('br'): - line_break.replaceWith(delimiter) + delimiter = "\n" + for line_break in soup.findAll("br"): + line_break.replaceWith(delimiter) description = soup.get_text() last_modified = self.get_item_from_vevent(event, "LAST_MODIFIED") @@ -103,17 +102,21 @@ class ICALExtractor(Extractor): if related_to is not None: if related_to in self.uuids: self.uuids[related_to] += 1 - uuidrel = url + "#" + related_to + ":{:04}".format(self.uuids[related_to] - 1) + uuidrel = ( + url + + "#" + + related_to + + ":{:04}".format(self.uuids[related_to] - 1) + ) # possible limitation: if the ordering is not original then related - tags = self.default_value_if_exists(default_values, "tags") - last_modified = self.get_item_from_vevent(event, "LAST-MODIFIED", raw = True) + last_modified = self.get_item_from_vevent(event, "LAST-MODIFIED", raw=True) recurrence_entries = {} for e in ["RRULE", "EXRULE", "EXDATE", "RDATE"]: - i = self.get_item_from_vevent(event, e, raw = True) + i = self.get_item_from_vevent(event, e, raw=True) if i is not None: recurrence_entries[e] = i @@ -122,7 +125,10 @@ class ICALExtractor(Extractor): for k, r in recurrence_entries.items(): if isinstance(r, list): - recurrences += "\n".join([k + ":" + e.to_ical().decode() for e in r]) + "\n" + recurrences += ( + "\n".join([k + ":" + e.to_ical().decode() for e in r]) + + "\n" + ) else: recurrences += k + ":" + r.to_ical().decode() + "\n" else: @@ -132,25 +138,74 @@ class ICALExtractor(Extractor): luuids = [event_url] if uuidrel is not None: luuids += [uuidrel] - self.add_event(title, category, start_day, location, description, tags, recurrences=recurrences, uuids=luuids, url_human=url_human, start_time=start_time, end_day=end_day, end_time=end_time, last_modified=last_modified, published=published) - + self.add_event( + title, + category, + start_day, + location, + description, + tags, + recurrences=recurrences, + uuids=luuids, + url_human=url_human, + start_time=start_time, + end_day=end_day, + end_time=end_time, + last_modified=last_modified, + published=published, + ) return self.get_structure() # A variation on ICAL extractor that removes any even named "Busy" class ICALNoBusyExtractor(ICALExtractor): - - def add_event(self, title, category, start_day, location, description, tags, uuids, recurrences=None, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False, image=None, image_alt=None): - if title != 'Busy': - super().add_event(title, category, start_day, location, description, tags, uuids, recurrences, url_human, start_time, end_day, end_time, last_modified, published, image, image_alt) + def add_event( + self, + title, + category, + start_day, + location, + description, + tags, + uuids, + recurrences=None, + url_human=None, + start_time=None, + end_day=None, + end_time=None, + last_modified=None, + published=False, + image=None, + image_alt=None, + ): + if title != "Busy": + super().add_event( + title, + category, + start_day, + location, + description, + tags, + uuids, + recurrences, + url_human, + start_time, + end_day, + end_time, + last_modified, + published, + image, + image_alt, + ) # A variation on ICAL extractor that remove any visual composer anchors class ICALNoVCExtractor(ICALExtractor): - def __init__(self): - self.parser = bbcode.Parser(newline="\n", drop_unrecognized=True, install_defaults=False) + self.parser = bbcode.Parser( + newline="\n", drop_unrecognized=True, install_defaults=False + ) self.parser.add_simple_formatter("vc_row", "%(value)s") self.parser.add_simple_formatter("vc_column", "%(value)s") self.parser.add_simple_formatter("vc_column_text", "%(value)s") @@ -164,5 +219,40 @@ class ICALNoVCExtractor(ICALExtractor): result = self.parser.format(text) return result - def add_event(self, title, category, start_day, location, description, tags, uuids, recurrences=None, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False, image=None, image_alt=None): - super().add_event(title, category, start_day, location, self.clean_vc(description), tags, uuids, recurrences, url_human, start_time, end_day, end_time, last_modified, published, image, image_alt) \ No newline at end of file + def add_event( + self, + title, + category, + start_day, + location, + description, + tags, + uuids, + recurrences=None, + url_human=None, + start_time=None, + end_day=None, + end_time=None, + last_modified=None, + published=False, + image=None, + image_alt=None, + ): + super().add_event( + title, + category, + start_day, + location, + self.clean_vc(description), + tags, + uuids, + recurrences, + url_human, + start_time, + end_day, + end_time, + last_modified, + published, + image, + image_alt, + ) diff --git a/src/agenda_culturel/import_tasks/generic_extractors.py b/src/agenda_culturel/import_tasks/generic_extractors.py index af2f08c..e7dd2fa 100644 --- a/src/agenda_culturel/import_tasks/generic_extractors.py +++ b/src/agenda_culturel/import_tasks/generic_extractors.py @@ -8,8 +8,8 @@ from django.utils.translation import gettext_lazy as _ from dateutil import parser import datetime -class GGCalendar: +class GGCalendar: def __init__(self, url): self.url = url self.extract_info() @@ -18,10 +18,10 @@ class GGCalendar: parsed_url = urlparse(self.url.replace("#", "%23")) params = parse_qs(parsed_url.query) - self.location = params['location'][0] if 'location' in params else None - self.title = params['text'][0] if 'text' in params else None - if 'dates' in params: - dates = [x.replace(" ", "+") for x in params['dates'][0].split("/")] + self.location = params["location"][0] if "location" in params else None + self.title = params["text"][0] if "text" in params else None + if "dates" in params: + dates = [x.replace(" ", "+") for x in params["dates"][0].split("/")] if len(dates) > 0: date = parser.parse(dates[0]) self.start_day = date.date() @@ -42,13 +42,11 @@ class GGCalendar: self.end_time = None - # A class to extract events from URL with two steps: # - first build a list of urls where the events will be found # - then for each document downloaded from these urls, build the events # This class is an abstract class class TwoStepsExtractor(Extractor): - def __init__(self): super().__init__() self.event_urls = None @@ -96,35 +94,83 @@ class TwoStepsExtractor(Extractor): self.event_properties[url] = {} self.event_properties[url]["location"] = loc - def add_event_with_props(self, event_url, title, category, start_day, location, description, tags, uuids, recurrences=None, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False, image=None, image_alt=None): - + def add_event_with_props( + self, + event_url, + title, + category, + start_day, + location, + description, + tags, + uuids, + recurrences=None, + url_human=None, + start_time=None, + end_day=None, + end_time=None, + last_modified=None, + published=False, + image=None, + image_alt=None, + ): if event_url in self.event_properties: - if 'tags' in self.event_properties[event_url]: - tags = tags + self.event_properties[event_url]['tags'] - if 'start_day' in self.event_properties[event_url]: - start_day = self.event_properties[event_url]['start_day'] - if 'start_time' in self.event_properties[event_url]: - start_time = self.event_properties[event_url]['start_time'] - if 'title' in self.event_properties[event_url]: - title = self.event_properties[event_url]['title'] - if 'category' in self.event_properties[event_url]: - category = self.event_properties[event_url]['category'] - if 'location' in self.event_properties[event_url]: - location = self.event_properties[event_url]['location'] - - self.add_event(title, category, start_day, location, description, tags, uuids, recurrences, url_human, start_time, end_day, end_time, last_modified, published, image, image_alt) + if "tags" in self.event_properties[event_url]: + tags = tags + self.event_properties[event_url]["tags"] + if "start_day" in self.event_properties[event_url]: + start_day = self.event_properties[event_url]["start_day"] + if "start_time" in self.event_properties[event_url]: + start_time = self.event_properties[event_url]["start_time"] + if "title" in self.event_properties[event_url]: + title = self.event_properties[event_url]["title"] + if "category" in self.event_properties[event_url]: + category = self.event_properties[event_url]["category"] + if "location" in self.event_properties[event_url]: + location = self.event_properties[event_url]["location"] + self.add_event( + title, + category, + start_day, + location, + description, + tags, + uuids, + recurrences, + url_human, + start_time, + end_day, + end_time, + last_modified, + published, + image, + image_alt, + ) @abstractmethod def build_event_url_list(self, content): pass @abstractmethod - def add_event_from_content(self, event_content, event_url, url_human = None, default_values = None, published = False): + def add_event_from_content( + self, + event_content, + event_url, + url_human=None, + default_values=None, + published=False, + ): pass - - def extract(self, content, url, url_human = None, default_values = None, published = False, only_future=True): + def extract( + self, + content, + url, + url_human=None, + default_values=None, + published=False, + only_future=True, + ): self.only_future = only_future self.now = datetime.datetime.now().date() self.set_header(url) @@ -133,24 +179,25 @@ class TwoStepsExtractor(Extractor): self.url = url self.event_urls = [] self.event_properties.clear() - + # first build the event list self.build_event_url_list(content) if self.event_urls is None: - raise Exception('Unable to find the event list from the main document') + raise Exception("Unable to find the event list from the main document") if self.downloader is None: - raise Exception('The downloader is not defined') + raise Exception("The downloader is not defined") # then process each element of the list for i, event_url in enumerate(self.event_urls): # first download the content associated with this link content_event = self.downloader.get_content(event_url) if content_event is None: - raise Exception(_('Cannot extract event from url {}').format(event_url)) + raise Exception(_("Cannot extract event from url {}").format(event_url)) # then extract event information from this html document - self.add_event_from_content(content_event, event_url, url_human, default_values, published) - - return self.get_structure() + self.add_event_from_content( + content_event, event_url, url_human, default_values, published + ) + return self.get_structure() diff --git a/src/agenda_culturel/import_tasks/importer.py b/src/agenda_culturel/import_tasks/importer.py index 1906d11..d289df5 100644 --- a/src/agenda_culturel/import_tasks/importer.py +++ b/src/agenda_culturel/import_tasks/importer.py @@ -5,15 +5,16 @@ from .extractor import * class URL2Events: - - def __init__(self, downloader = SimpleDownloader(), extractor = None, single_event=False): - + def __init__( + self, downloader=SimpleDownloader(), extractor=None, single_event=False + ): self.downloader = downloader self.extractor = extractor self.single_event = single_event - - def process(self, url, url_human = None, cache = None, default_values = None, published = False): + def process( + self, url, url_human=None, cache=None, default_values=None, published=False + ): content = self.downloader.get_content(url, cache) if content is None: @@ -21,13 +22,14 @@ class URL2Events: if self.extractor is not None: self.extractor.set_downloader(self.downloader) - return self.extractor.extract(content, url, url_human, default_values, published) + return self.extractor.extract( + content, url, url_human, default_values, published + ) else: # if the extractor is not defined, use a list of default extractors for e in Extractor.get_default_extractors(self.single_event): - e.set_downloader(self.downloader) - events = e.extract(content, url, url_human, default_values, published) - if events is not None: - return events + e.set_downloader(self.downloader) + events = e.extract(content, url, url_human, default_values, published) + if events is not None: + return events return None - diff --git a/src/agenda_culturel/models.py b/src/agenda_culturel/models.py index 3d0303b..174d2d1 100644 --- a/src/agenda_culturel/models.py +++ b/src/agenda_culturel/models.py @@ -28,22 +28,33 @@ from location_field.models.plain import PlainLocationField from .calendar import CalendarList, CalendarDay import logging + logger = logging.getLogger(__name__) + def remove_accents(input_str): - nfkd_form = unicodedata.normalize('NFKD', input_str) - return u"".join([c for c in nfkd_form if not unicodedata.combining(c)]) + nfkd_form = unicodedata.normalize("NFKD", input_str) + return "".join([c for c in nfkd_form if not unicodedata.combining(c)]) + class StaticContent(models.Model): - - name = models.CharField(verbose_name=_('Name'), help_text=_('Category name'), max_length=512, unique=True) - text = RichTextField(verbose_name=_('Content'), help_text=_('Text as shown to the visitors')) - url_path = models.CharField(verbose_name=_('URL path'), help_text=_('URL path where the content is included.')) + name = models.CharField( + verbose_name=_("Name"), + help_text=_("Category name"), + max_length=512, + unique=True, + ) + text = RichTextField( + verbose_name=_("Content"), help_text=_("Text as shown to the visitors") + ) + 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') - + verbose_name = _("Static content") + verbose_name_plural = _("Static contents") def __str__(self): return self.name @@ -51,8 +62,8 @@ class StaticContent(models.Model): def get_absolute_url(self): return self.url_path -class Category(models.Model): +class Category(models.Model): default_name = "Sans catégorie" default_alt_name = "Événements non catégorisés" default_codename = "∅" @@ -68,12 +79,28 @@ class Category(models.Model): ("#bdcf32", "color 6"), ("#87bc45", "color 7"), ("#27aeef", "color 8"), - ("#b33dc6", "color 9")] + ("#b33dc6", "color 9"), + ] - name = models.CharField(verbose_name=_('Name'), help_text=_('Category name'), max_length=512) - alt_name = models.CharField(verbose_name=_('Alternative Name'), help_text=_('Alternative name used with a time period'), max_length=512) - codename = models.CharField(verbose_name=_('Short name'), help_text=_('Short name of the category'), max_length=3) - color = ColorField(verbose_name=_('Color'), help_text=_('Color used as background for the category'), blank=True, null=True) + name = models.CharField( + verbose_name=_("Name"), help_text=_("Category name"), max_length=512 + ) + alt_name = models.CharField( + verbose_name=_("Alternative Name"), + help_text=_("Alternative name used with a time period"), + max_length=512, + ) + codename = models.CharField( + verbose_name=_("Short name"), + help_text=_("Short name of the category"), + max_length=3, + ) + color = ColorField( + verbose_name=_("Color"), + help_text=_("Color used as background for the category"), + blank=True, + null=True, + ) def save(self, *args, **kwargs): if self.color is None: @@ -92,16 +119,17 @@ class Category(models.Model): def get_default_category(): try: - default, created = Category.objects.get_or_create(name=Category.default_name, + default, created = Category.objects.get_or_create( + name=Category.default_name, alt_name=Category.default_alt_name, codename=Category.default_codename, - color=Category.default_color) + color=Category.default_color, + ) return default except: return None - def get_default_category_id(): cat = Category.get_default_category() if cat: @@ -112,21 +140,18 @@ class Category(models.Model): def css_class(self): return "cat-" + str(self.id) - def __str__(self): return self.name class Meta: - verbose_name = _('Category') - verbose_name_plural = _('Categories') + verbose_name = _("Category") + verbose_name_plural = _("Categories") class DuplicatedEvents(models.Model): - class Meta: - verbose_name = _('Duplicated events') - verbose_name_plural = _('Duplicated events') - + verbose_name = _("Duplicated events") + verbose_name_plural = _("Duplicated events") def nb_duplicated(self): return Event.objects.filter(possibly_duplicated=self).count() @@ -159,7 +184,9 @@ class DuplicatedEvents(models.Model): return Event.get_comparison(self.get_duplicated()) def remove_singletons(): - singletons = DuplicatedEvents.objects.annotate(nb_events=Count("event")).filter(nb_events__lte=1) + singletons = DuplicatedEvents.objects.annotate(nb_events=Count("event")).filter( + nb_events__lte=1 + ) nb = len(singletons) if nb > 0: logger.warning("Removing: " + str(nb) + " empty or singleton duplicated") @@ -183,18 +210,30 @@ class DuplicatedEvents(models.Model): return nb - 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) - city = models.CharField(verbose_name=_('City'), help_text=_('City name')) - location = PlainLocationField(based_fields=['name', 'address', 'city'], zoom=12) - - 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) + 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, + ) + city = models.CharField(verbose_name=_("City"), help_text=_("City name")) + location = PlainLocationField(based_fields=["name", "address", "city"], zoom=12) + + 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') + verbose_name = _("Place") + verbose_name_plural = _("Places") def __str__(self): if self.address: @@ -228,18 +267,21 @@ class Place(models.Model): Event.objects.bulk_update(to_be_updated, fields=["exact_location"]) return len(to_be_updated) - def get_all_cities(): try: - tags = list([p["city"] for p in Place.objects.values("city").distinct().order_by("city")]) + tags = list( + [ + p["city"] + for p in Place.objects.values("city").distinct().order_by("city") + ] + ) except: tags = [] return tags class Event(models.Model): - - class STATUS(models.TextChoices): + class STATUS(models.TextChoices): PUBLISHED = "published", _("Published") DRAFT = "draft", _("Draft") TRASH = "trash", _("Trash") @@ -252,37 +294,133 @@ class Event(models.Model): recurrence_dtstart = models.DateTimeField(editable=False, blank=True, null=True) recurrence_dtend = models.DateTimeField(editable=False, blank=True, null=True) - title = models.CharField(verbose_name=_('Title'), help_text=_('Short title'), max_length=512) + title = models.CharField( + verbose_name=_("Title"), help_text=_("Short title"), max_length=512 + ) - status = models.CharField(_("Status"), max_length=20, choices=STATUS.choices, default=STATUS.DRAFT) + status = models.CharField( + _("Status"), max_length=20, choices=STATUS.choices, default=STATUS.DRAFT + ) - category = models.ForeignKey(Category, verbose_name=_('Category'), help_text=_('Category of the event'), null=True, default=Category.get_default_category_id(), on_delete=models.SET_DEFAULT) + category = models.ForeignKey( + Category, + verbose_name=_("Category"), + help_text=_("Category of the event"), + null=True, + default=Category.get_default_category_id(), + on_delete=models.SET_DEFAULT, + ) - start_day = models.DateField(verbose_name=_('Day of the event'), help_text=_('Day of the event')) - start_time = models.TimeField(verbose_name=_('Starting time'), help_text=_('Starting time'), blank=True, null=True) + start_day = models.DateField( + verbose_name=_("Day of the event"), help_text=_("Day of the event") + ) + start_time = models.TimeField( + verbose_name=_("Starting time"), + help_text=_("Starting time"), + blank=True, + null=True, + ) - end_day = models.DateField(verbose_name=_('End day of the event'), help_text=_('End day of the event, only required if different from the start day.'), blank=True, null=True) - end_time = models.TimeField(verbose_name=_('Final time'), help_text=_('Final time'), blank=True, null=True) + end_day = models.DateField( + verbose_name=_("End day of the event"), + help_text=_( + "End day of the event, only required if different from the start day." + ), + blank=True, + null=True, + ) + end_time = models.TimeField( + verbose_name=_("Final time"), help_text=_("Final time"), blank=True, null=True + ) - recurrences = recurrence.fields.RecurrenceField(verbose_name=_("Recurrence"), include_dtstart=False, blank=True, null=True) + recurrences = recurrence.fields.RecurrenceField( + verbose_name=_("Recurrence"), include_dtstart=False, blank=True, null=True + ) - exact_location = models.ForeignKey(Place, verbose_name=_('Location'), help_text=_('Address of the event'), null=True, on_delete=models.SET_NULL, blank=True) - location = models.CharField(verbose_name=_('Location (free form)'), help_text=_('Address of the event in case its not available in the already known places (free form)'), max_length=512, default="") + exact_location = models.ForeignKey( + Place, + verbose_name=_("Location"), + help_text=_("Address of the event"), + null=True, + on_delete=models.SET_NULL, + blank=True, + ) + location = models.CharField( + verbose_name=_("Location (free form)"), + help_text=_( + "Address of the event in case its not available in the already known places (free form)" + ), + max_length=512, + default="", + ) - description = models.TextField(verbose_name=_('Description'), help_text=_('General description of the event'), blank=True, null=True) + description = models.TextField( + verbose_name=_("Description"), + help_text=_("General description of the event"), + blank=True, + null=True, + ) - local_image = models.ImageField(verbose_name=_('Illustration (local image)'), help_text=_("Illustration image stored in the agenda server"), max_length=1024, blank=True, null=True) + local_image = models.ImageField( + verbose_name=_("Illustration (local image)"), + help_text=_("Illustration image stored in the agenda server"), + max_length=1024, + blank=True, + null=True, + ) - image = models.URLField(verbose_name=_('Illustration'), help_text=_("URL of the illustration image"), max_length=1024, blank=True, null=True) - image_alt = models.CharField(verbose_name=_('Illustration description'), help_text=_('Alternative text used by screen readers for the image'), blank=True, null=True, max_length=1024) + image = models.URLField( + verbose_name=_("Illustration"), + help_text=_("URL of the illustration image"), + max_length=1024, + blank=True, + null=True, + ) + image_alt = models.CharField( + verbose_name=_("Illustration description"), + help_text=_("Alternative text used by screen readers for the image"), + blank=True, + null=True, + max_length=1024, + ) - import_sources = ArrayField(models.CharField(max_length=512), verbose_name=_('Importation source'), help_text=_("Importation source used to detect removed entries."), blank=True, null=True) - uuids = ArrayField(models.CharField(max_length=512), verbose_name=_('UUIDs'), help_text=_("UUIDs from import to detect duplicated entries."), blank=True, null=True) - reference_urls = ArrayField(models.URLField(max_length=512), verbose_name=_('URLs'), help_text=_("List of all the urls where this event can be found."), blank=True, null=True) + import_sources = ArrayField( + models.CharField(max_length=512), + verbose_name=_("Importation source"), + help_text=_("Importation source used to detect removed entries."), + blank=True, + null=True, + ) + uuids = ArrayField( + models.CharField(max_length=512), + verbose_name=_("UUIDs"), + help_text=_("UUIDs from import to detect duplicated entries."), + blank=True, + null=True, + ) + reference_urls = ArrayField( + models.URLField(max_length=512), + verbose_name=_("URLs"), + help_text=_("List of all the urls where this event can be found."), + blank=True, + null=True, + ) - tags = ArrayField(models.CharField(max_length=64), verbose_name=_('Tags'), help_text=_("A list of tags that describe the event."), blank=True, null=True) + tags = ArrayField( + models.CharField(max_length=64), + verbose_name=_("Tags"), + help_text=_("A list of tags that describe the event."), + blank=True, + null=True, + ) - possibly_duplicated = models.ForeignKey(DuplicatedEvents, verbose_name=_('Possibly duplicated'), on_delete=models.SET_NULL, null=True, blank=True) + possibly_duplicated = models.ForeignKey( + DuplicatedEvents, + verbose_name=_("Possibly duplicated"), + on_delete=models.SET_NULL, + null=True, + blank=True, + ) def get_consolidated_end_day(self, intuitive=True): if intuitive: @@ -299,14 +437,12 @@ class Event(models.Model): last = self.get_consolidated_end_day() return [first + timedelta(n) for n in range(int((last - first).days) + 1)] - def get_nb_events_same_dates(self): first = self.start_day last = self.get_consolidated_end_day() calendar = CalendarList(first, last, exact=True) return [(len(d.events), d.date) for dstr, d in calendar.calendar_days.items()] - def is_single_day(self, intuitive=True): return self.start_day == self.get_consolidated_end_day(intuitive) @@ -314,22 +450,28 @@ class Event(models.Model): return d >= self.start_day and d <= self.get_consolidated_end_day(intuitive) def get_absolute_url(self): - return reverse("view_event", kwargs={"year": self.start_day.year, - "month": self.start_day.month, - "day": self.start_day.day, - "pk": self.pk, "extra": slugify(self.title)}) + return reverse( + "view_event", + kwargs={ + "year": self.start_day.year, + "month": self.start_day.month, + "day": self.start_day.day, + "pk": self.pk, + "extra": slugify(self.title), + }, + ) def __str__(self): return _date(self.start_day) + ": " + self.title class Meta: - verbose_name = _('Event') - verbose_name_plural = _('Events') + verbose_name = _("Event") + verbose_name_plural = _("Events") permissions = [("set_duplicated_event", "Can set an event as duplicated")] def get_all_tags(): try: - tags = list(Event.objects.values_list('tags', flat = True)) + tags = list(Event.objects.values_list("tags", flat=True)) except: tags = [] uniq_tags = set() @@ -348,15 +490,17 @@ class Event(models.Model): return self.status == Event.STATUS.TRASH def modified(self): - return self.modified_date is None or abs((self.modified_date - self.created_date).total_seconds()) > 1 + return ( + self.modified_date is None + or abs((self.modified_date - self.created_date).total_seconds()) > 1 + ) def nb_draft_events(): return Event.objects.filter(status=Event.STATUS.DRAFT).count() - def download_image(self): # first download file - + a = urlparse(self.image) basename = os.path.basename(a.path) @@ -390,24 +534,29 @@ class Event(models.Model): self.modified_date = now def get_recurrence_at_date(self, year, month, day): - dtstart = timezone.make_aware(datetime(year, month, day, 0, 0), timezone.get_default_timezone()) + dtstart = timezone.make_aware( + datetime(year, month, day, 0, 0), timezone.get_default_timezone() + ) recurrences = self.get_recurrences_between(dtstart, dtstart) if len(recurrences) == 0: return self else: return recurrences[0] - # return a copy of the current object for each recurrence between first an last date (included) def get_recurrences_between(self, firstdate, lastdate): if not self.has_recurrences(): return [self] else: result = [] - dtstart = timezone.make_aware(datetime.combine(self.start_day, time()), timezone.get_default_timezone()) + dtstart = timezone.make_aware( + datetime.combine(self.start_day, time()), + timezone.get_default_timezone(), + ) self.recurrences.dtstart = dtstart - for d in self.recurrences.between(firstdate, lastdate, inc=True, dtstart=dtstart): - + for d in self.recurrences.between( + firstdate, lastdate, inc=True, dtstart=dtstart + ): c = copy.deepcopy(self) c.start_day = d.date() if c.end_day is not None: @@ -422,10 +571,30 @@ class Event(models.Model): return self.recurrences is not None and len(self.recurrences.rrules) != 0 def update_recurrence_dtstartend(self): - sday = date.fromisoformat(self.start_day) if isinstance(self.start_day, str) else self.start_day - eday = date.fromisoformat(self.end_day) if isinstance(self.end_day, str) else self.end_day - stime = time.fromisoformat(self.start_time) if isinstance(self.start_time, str) else time() if self.start_time is None else self.start_time - etime = time.fromisoformat(self.end_time) if isinstance(self.end_time, str) else time() if self.end_time is None else self.end_time + sday = ( + date.fromisoformat(self.start_day) + if isinstance(self.start_day, str) + else self.start_day + ) + eday = ( + date.fromisoformat(self.end_day) + if isinstance(self.end_day, str) + else self.end_day + ) + stime = ( + time.fromisoformat(self.start_time) + if isinstance(self.start_time, str) + else time() + if self.start_time is None + else self.start_time + ) + etime = ( + time.fromisoformat(self.end_time) + if isinstance(self.end_time, str) + else time() + if self.end_time is None + else self.end_time + ) self.recurrence_dtstart = datetime.combine(sday, stime) if not self.has_recurrences(): @@ -434,19 +603,26 @@ class Event(models.Model): else: self.recurrence_dtend = datetime.combine(eday, etime) else: - if self.recurrences.rrules[0].until is None and self.recurrences.rrules[0].count is None: + if ( + self.recurrences.rrules[0].until is None + and self.recurrences.rrules[0].count is None + ): self.recurrence_dtend = None else: self.recurrences.dtstart = datetime.combine(sday, time()) occurrence = self.recurrences.occurrences() try: self.recurrence_dtend = occurrence[-1] - if self.recurrences.dtend is not None and self.recurrences.dtstart is not None: - self.recurrence_dtend += self.recurrences.dtend - self.recurrences.dtstart + if ( + self.recurrences.dtend is not None + and self.recurrences.dtstart is not None + ): + self.recurrence_dtend += ( + self.recurrences.dtend - self.recurrences.dtstart + ) except: self.recurrence_dtend = self.recurrence_dtstart - def prepare_save(self): self.update_modification_dates() @@ -466,13 +642,15 @@ class Event(models.Model): self.exact_location = p break - def save(self, *args, **kwargs): - self.prepare_save() # check for similar events if no duplicated is known only if the event is created - if self.pk is None and self.possibly_duplicated is None and not self.is_skip_duplicate_check(): + if ( + self.pk is None + and self.possibly_duplicated is None + and not self.is_skip_duplicate_check() + ): # and if this is not an importation process if not self.is_in_importation_process(): similar_events = self.find_similar_events() @@ -481,19 +659,21 @@ class Event(models.Model): if len(similar_events) != 0: self.set_possibly_duplicated(similar_events) - # delete duplicated group if it's only with one element - if self.possibly_duplicated is not None and self.possibly_duplicated.nb_duplicated() == 1: + if ( + self.possibly_duplicated is not None + and self.possibly_duplicated.nb_duplicated() == 1 + ): self.possibly_duplicated.delete() self.possibly_duplicated = None - super().save(*args, **kwargs) - - def from_structure(event_structure, import_source = None): + def from_structure(event_structure, import_source=None): if "category" in event_structure and event_structure["category"] is not None: - event_structure["category"] = Category.objects.get(name=event_structure["category"]) + event_structure["category"] = Category.objects.get( + name=event_structure["category"] + ) if "published" in event_structure and event_structure["published"] is not None: if event_structure["published"]: @@ -503,12 +683,15 @@ class Event(models.Model): del event_structure["published"] else: event_structure["status"] = Event.STATUS.DRAFT - + if "url_human" in event_structure and event_structure["url_human"] is not None: event_structure["reference_urls"] = [event_structure["url_human"]] del event_structure["url_human"] - if "last_modified" in event_structure and event_structure["last_modified"] is not None: + if ( + "last_modified" in event_structure + and event_structure["last_modified"] is not None + ): d = datetime.fromisoformat(event_structure["last_modified"]) if d.year == 2024 and d.month > 2: logger.warning("last modified {}".format(d)) @@ -520,10 +703,14 @@ class Event(models.Model): event_structure["modified_date"] = None if "start_time" in event_structure: - event_structure["start_time"] = time.fromisoformat(event_structure["start_time"]) + event_structure["start_time"] = time.fromisoformat( + event_structure["start_time"] + ) if "end_time" in event_structure: - event_structure["end_time"] = time.fromisoformat(event_structure["end_time"]) + event_structure["end_time"] = time.fromisoformat( + event_structure["end_time"] + ) if "location" not in event_structure or event_structure["location"] is None: event_structure["location"] = "" @@ -531,10 +718,21 @@ class Event(models.Model): if "description" in event_structure and event_structure["description"] is None: event_structure["description"] = "" - if "recurrences" in event_structure and event_structure["recurrences"] is not None: - event_structure["recurrences"] = recurrence.deserialize(event_structure["recurrences"]) - event_structure["recurrences"].exdates = [e.replace(hour=0, minute=0, second=0) for e in event_structure["recurrences"].exdates] - event_structure["recurrences"].rdates = [e.replace(hour=0, minute=0, second=0) for e in event_structure["recurrences"].rdates] + if ( + "recurrences" in event_structure + and event_structure["recurrences"] is not None + ): + event_structure["recurrences"] = recurrence.deserialize( + event_structure["recurrences"] + ) + event_structure["recurrences"].exdates = [ + e.replace(hour=0, minute=0, second=0) + for e in event_structure["recurrences"].exdates + ] + event_structure["recurrences"].rdates = [ + e.replace(hour=0, minute=0, second=0) + for e in event_structure["recurrences"].rdates + ] else: event_structure["recurrences"] = None @@ -544,7 +742,6 @@ class Event(models.Model): return Event(**event_structure) - def find_similar_events(self): start_time_test = Q(start_time=self.start_time) @@ -552,21 +749,38 @@ class Event(models.Model): # convert str start_time to time if isinstance(self.start_time, str): self.start_time = time.fromisoformat(self.start_time) - interval = (time(self.start_time.hour - 1, self.start_time.minute) if self.start_time.hour >= 1 else time(0, 0), - time(self.start_time.hour + 1, self.start_time.minute) if self.start_time.hour < 23 else time(23, 59)) + interval = ( + time(self.start_time.hour - 1, self.start_time.minute) + if self.start_time.hour >= 1 + else time(0, 0), + time(self.start_time.hour + 1, self.start_time.minute) + if self.start_time.hour < 23 + else time(23, 59), + ) start_time_test = start_time_test | Q(start_time__range=interval) - return Event.objects.annotate(similarity_title=TrigramSimilarity("title", self.title)). \ - annotate(similarity_location=TrigramSimilarity("location", self.location)). \ - filter(Q(start_day=self.start_day) & start_time_test & Q(similarity_title__gt=0.5) & Q(similarity_title__gt=0.3)) - + return ( + Event.objects.annotate( + similarity_title=TrigramSimilarity("title", self.title) + ) + .annotate(similarity_location=TrigramSimilarity("location", self.location)) + .filter( + Q(start_day=self.start_day) + & start_time_test + & Q(similarity_title__gt=0.5) + & Q(similarity_title__gt=0.3) + ) + ) def find_same_events_by_uuid(self): - return None if self.uuids is None or len(self.uuids) == 0 else Event.objects.filter(uuids__contains=self.uuids) - + return ( + None + if self.uuids is None or len(self.uuids) == 0 + else Event.objects.filter(uuids__contains=self.uuids) + ) def split_uuid(uuid): - els = uuid.split(':') + els = uuid.split(":") if len(els) == 1: return ":".join(els[0:-1]), 0 else: @@ -594,8 +808,9 @@ class Event(models.Model): if self.possibly_duplicated is None: return [] else: - return Event.objects.filter(possibly_duplicated=self.possibly_duplicated).exclude(pk=self.pk) - + return Event.objects.filter( + possibly_duplicated=self.possibly_duplicated + ).exclude(pk=self.pk) def get_comparison(events): result = [] @@ -608,7 +823,7 @@ class Event(models.Model): else: result.append({"similar": False, "key": attr, "values": values}) return result - + def similar(self, event): res = Event.get_comparison([self, event]) for r in res: @@ -616,14 +831,13 @@ class Event(models.Model): return False return True - def set_possibly_duplicated(self, events): - # get existing groups - groups = list(set([e.possibly_duplicated for e in events] + [self.possibly_duplicated])) + groups = list( + set([e.possibly_duplicated for e in events] + [self.possibly_duplicated]) + ) groups = [g for g in groups if g is not None] - # do we have to create a new group? if len(groups) == 0: group = DuplicatedEvents.objects.create() @@ -632,7 +846,6 @@ class Event(models.Model): group = DuplicatedEvents.merge_groups(groups) group.save() - # set the possibly duplicated group for the current object self.possibly_duplicated = group @@ -644,14 +857,23 @@ class Event(models.Model): elist = list(events) + ([self] if self.pk is not None else []) Event.objects.bulk_update(elist, fields=["possibly_duplicated"]) - def data_fields(all=False, local_img=True): if all: result = ["category"] else: result = [] - result += ["title", "location", "exact_location", "start_day", "start_time", "end_day", "end_time", "description", "image"] + result += [ + "title", + "location", + "exact_location", + "start_day", + "start_time", + "end_day", + "end_time", + "description", + "image", + ] if all and local_img: result += ["local_image"] result += ["image_alt", "reference_urls", "recurrences"] @@ -668,7 +890,6 @@ class Event(models.Model): def find_same_event_by_data_in_list(self, events): return [e for e in events if self.same_event_by_data(e)] - def find_last_imported(events): events = [e for e in events if e.imported_date is not None] if len(events) == 0: @@ -677,16 +898,19 @@ class Event(models.Model): events.sort(key=lambda e: e.imported_date, reverse=True) return events[0] - def find_last_imported_not_modified(events): - events = [e for e in events if e.imported_date is not None and (e.modified_date is None or e.modified_date <= e.imported_date)] + events = [ + e + for e in events + if e.imported_date is not None + and (e.modified_date is None or e.modified_date <= e.imported_date) + ] if len(events) == 0: return None else: events.sort(key=lambda e: e.imported_date, reverse=True) return events[0] - def update(self, other): # TODO: what about category, tags? # set attributes @@ -708,7 +932,6 @@ class Event(models.Model): if not uuid in self.uuids: self.uuids.append(uuid) - # Limitation: the given events should not be considered similar one to another... def import_events(events, remove_missing_from_source=None): to_import = [] @@ -744,7 +967,9 @@ class Event(models.Model): if len(same_events) != 0: # check if one event has been imported and not modified in this list same_imported = Event.find_last_imported_not_modified(same_events) - same_events_not_similar = [e for e in same_events if not e.similar(event)] + same_events_not_similar = [ + e for e in same_events if not e.similar(event) + ] if same_imported or len(same_events_not_similar) == 0: if not same_imported: same_imported = Event.find_last_imported(same_events) @@ -766,7 +991,6 @@ class Event(models.Model): # if it exists similar events, add this relation to the event if len(similar_events) != 0: - # check if an event from the list is exactly the same as the new one (using data) same_events = event.find_same_event_by_data_in_list(similar_events) if same_events is not None and len(same_events) > 0: @@ -785,15 +1009,33 @@ class Event(models.Model): # then import all the new events imported = Event.objects.bulk_create(to_import) - nb_updated = Event.objects.bulk_update(to_update, fields = Event.data_fields() + ["imported_date", "modified_date", "uuids", "status"]) + nb_updated = Event.objects.bulk_update( + to_update, + fields=Event.data_fields() + + ["imported_date", "modified_date", "uuids", "status"], + ) nb_draft = 0 if remove_missing_from_source is not None and max_date is not None: # events that are missing from the import but in database are turned into drafts # only if they are in the future - in_interval = Event.objects.filter(((Q(end_day__isnull=True) & Q(start_day__gte=min_date) & Q(start_day__lte=max_date)) | - (Q(end_day__isnull=False) & ~(Q(start_day__gt=max_date) | Q(end_day__lt=min_date)))) & Q(import_sources__contains=[remove_missing_from_source]) & Q(status=Event.STATUS.PUBLISHED) & Q(uuids__len__gt=0)) + in_interval = Event.objects.filter( + ( + ( + Q(end_day__isnull=True) + & Q(start_day__gte=min_date) + & Q(start_day__lte=max_date) + ) + | ( + Q(end_day__isnull=False) + & ~(Q(start_day__gt=max_date) | Q(end_day__lt=min_date)) + ) + ) + & Q(import_sources__contains=[remove_missing_from_source]) + & Q(status=Event.STATUS.PUBLISHED) + & Q(uuids__len__gt=0) + ) to_draft = [] for e in in_interval: @@ -802,14 +1044,13 @@ class Event(models.Model): e.prepare_save() to_draft.append(e) - nb_draft = Event.objects.bulk_update(to_draft, fields = ["status"]) + nb_draft = Event.objects.bulk_update(to_draft, fields=["status"]) return imported, nb_updated, nb_draft def set_current_date(self, date): self.current_date = date - def get_start_end_datetimes(self, day): if self.start_day == day: if self.start_time is None: @@ -830,11 +1071,17 @@ class Event(models.Model): dtend = datetime.combine(day, time().max) return dtstart, dtend - + def get_concurrent_events(self): day = self.current_date if hasattr(self, "current_date") else self.start_day day_events = CalendarDay(self.start_day).get_events() - return [e for e in day_events if e != self and self.is_concurrent_event(e, day) and e.status == Event.STATUS.PUBLISHED] + return [ + e + for e in day_events + if e != self + and self.is_concurrent_event(e, day) + and e.status == Event.STATUS.PUBLISHED + ] def is_concurrent_event(self, e, day): dtstart, dtend = self.get_start_end_datetimes(day) @@ -843,68 +1090,146 @@ class Event(models.Model): return (dtstart <= e_dtstart <= dtend) or (e_dtstart <= dtstart <= e_dtend) - - class ContactMessage(models.Model): - class Meta: - verbose_name = _('Contact message') - verbose_name_plural = _('Contact messages') + verbose_name = _("Contact message") + verbose_name_plural = _("Contact messages") - subject = models.CharField(verbose_name=_('Subject'), help_text=_('The subject of your message'), max_length=512) - 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 = RichTextField(verbose_name=_('Message'), help_text=_('Your message')) + subject = models.CharField( + verbose_name=_("Subject"), + help_text=_("The subject of your message"), + max_length=512, + ) + 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 = RichTextField(verbose_name=_("Message"), help_text=_("Your message")) date = models.DateTimeField(auto_now_add=True) - closed = models.BooleanField(verbose_name=_('Closed'), help_text=_('this message has been processed and no longer needs to be handled'), default=False) - comments = RichTextField(verbose_name=_('Comments'), help_text=_('Comments on the message from the moderation team'), default="", blank=True, null=True) + closed = models.BooleanField( + verbose_name=_("Closed"), + help_text=_( + "this message has been processed and no longer needs to be handled" + ), + default=False, + ) + comments = RichTextField( + verbose_name=_("Comments"), + help_text=_("Comments on the message from the moderation team"), + default="", + blank=True, + null=True, + ) def nb_open_contactmessages(): return ContactMessage.objects.filter(closed=False).count() class RecurrentImport(models.Model): - - class Meta: - verbose_name = _('Recurrent import') - verbose_name_plural = _('Recurrent imports') + 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") - LACOOPE = "lacoope", _('lacoope.org') - LACOMEDIE = "lacomedie", _('la comédie') - LEFOTOMAT = "lefotomat", _('le fotomat') - LAPUCEALOREILLE = "lapucealoreille", _('la puce à l''oreille') + LACOOPE = "lacoope", _("lacoope.org") + LACOMEDIE = "lacomedie", _("la comédie") + LEFOTOMAT = "lefotomat", _("le fotomat") + LAPUCEALOREILLE = "lapucealoreille", _("la puce à l" "oreille") class DOWNLOADER(models.TextChoices): SIMPLE = "simple", _("simple") CHROMIUMHEADLESS = "chromium headless", _("Headless Chromium") - class RECURRENCE(models.TextChoices): - DAILY = "daily", _("daily"), + DAILY = ( + "daily", + _("daily"), + ) WEEKLY = "weekly", _("weekly") - 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) + 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) + 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"), + 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, + ) - source = models.URLField(verbose_name=_('Source'), help_text=_("URL of the source document"), 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) - defaultCategory = models.ForeignKey(Category, verbose_name=_('Category'), help_text=_('Category of each imported event'), default=Category.get_default_category_id(), 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) + 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, + ) + defaultCategory = models.ForeignKey( + Category, + verbose_name=_("Category"), + help_text=_("Category of each imported event"), + default=Category.get_default_category_id(), + 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 nb_imports(self): return BatchImportation.objects.filter(recurrentImport=self).count() @@ -914,62 +1239,125 @@ class RecurrentImport(models.Model): 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") + events = BatchImportation.objects.filter(recurrentImport=self).order_by( + "-created_date" + ) return events[0] class BatchImportation(models.Model): - - class STATUS(models.TextChoices): + 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') + verbose_name = _("Batch importation") + verbose_name_plural = _("Batch importations") permissions = [("run_batchimportation", "Can run a batch importation")] - 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) + 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, + ) - status = models.CharField(_("Status"), max_length=20, choices=STATUS.choices, default=STATUS.RUNNING) + 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) + 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) + 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="") class CategorisationRule(models.Model): + weight = models.IntegerField( + verbose_name=_("Weight"), + help_text=_("The lower is the weight, the earlier the filter is applied"), + default=0, + ) - weight = models.IntegerField(verbose_name=_('Weight'), help_text=_("The lower is the weight, the earlier the filter is applied"), default=0) + category = models.ForeignKey( + Category, + verbose_name=_("Category"), + help_text=_("Category applied to the event"), + on_delete=models.CASCADE, + ) - 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, + ) - 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, + ) - 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) + 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, + ) class Meta: - verbose_name = _('Categorisation rule') - verbose_name_plural = _('Categorisation rules') + 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): @@ -979,7 +1367,7 @@ class CategorisationRule(models.Model): if rule.match(event): event.category = rule.category return 1 - + return 0 def match_rules(event): @@ -988,17 +1376,18 @@ class CategorisationRule(models.Model): for rule in rules: if rule.match(event): return rule.category - + return None - 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 = remove_accents(self.description_contains).lower() in remove_accents(event.description).lower() + result = ( + remove_accents(self.description_contains).lower() + in remove_accents(event.description).lower() + ) if not result: return False @@ -1006,7 +1395,10 @@ class CategorisationRule(models.Model): 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() + result = ( + remove_accents(self.title_contains).lower() + in remove_accents(event.title).lower() + ) if not result: return False @@ -1014,7 +1406,10 @@ class CategorisationRule(models.Model): 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() + result = ( + remove_accents(self.location_contains).lower() + in remove_accents(event.location).lower() + ) if not result: return False @@ -1022,18 +1417,27 @@ class CategorisationRule(models.Model): class ModerationQuestion(models.Model): - - question = models.CharField(verbose_name=_('Question'), help_text=_('Text that will be shown to moderators'), max_length=512, unique=True) + question = models.CharField( + verbose_name=_("Question"), + help_text=_("Text that will be shown to moderators"), + max_length=512, + unique=True, + ) class Meta: - verbose_name = _('Moderation question') - verbose_name_plural = _('Moderation questions') - permissions = [("use_moderation_question", "Can use a moderation question to tag an event")] - + verbose_name = _("Moderation question") + verbose_name_plural = _("Moderation questions") + permissions = [ + ("use_moderation_question", "Can use a moderation question to tag an event") + ] def __str__(self): char_limit = 30 - return (self.question[:char_limit] + "...") if char_limit < len(self.question) else self.question + return ( + (self.question[:char_limit] + "...") + if char_limit < len(self.question) + else self.question + ) def get_absolute_url(self): return reverse("view_mquestion", kwargs={"pk": self.pk}) @@ -1043,24 +1447,54 @@ class ModerationQuestion(models.Model): class ModerationAnswer(models.Model): + question = models.ForeignKey( + ModerationQuestion, + related_name="answers", + verbose_name=_("Question"), + help_text=_("Associated question from moderation"), + on_delete=models.CASCADE, + ) - question = models.ForeignKey(ModerationQuestion, related_name="answers", verbose_name=_('Question'), help_text=_('Associated question from moderation'), on_delete=models.CASCADE) - - answer = models.CharField(verbose_name=_('Answer'), help_text=_('Text that will be shown to moderators'), max_length=512) - - adds_tags = ArrayField(models.CharField(max_length=64), verbose_name=_('Adds tags'), help_text=_("A list of tags that will be added if you choose this answer."), blank=True, null=True) - removes_tags = ArrayField(models.CharField(max_length=64), verbose_name=_('Removes tags'), help_text=_("A list of tags that will be removed if you choose this answer."), blank=True, null=True) + answer = models.CharField( + verbose_name=_("Answer"), + help_text=_("Text that will be shown to moderators"), + max_length=512, + ) + adds_tags = ArrayField( + models.CharField(max_length=64), + verbose_name=_("Adds tags"), + help_text=_("A list of tags that will be added if you choose this answer."), + blank=True, + null=True, + ) + removes_tags = ArrayField( + models.CharField(max_length=64), + verbose_name=_("Removes tags"), + help_text=_("A list of tags that will be removed if you choose this answer."), + blank=True, + null=True, + ) def complete_id(self): - return "answer_" + str(self.question.pk) + '_' + str(self.pk) + return "answer_" + str(self.question.pk) + "_" + str(self.pk) def html_description(self): result = self.answer + '
    ' if self.adds_tags: - result += ' '.join(['' + a + '' for a in self.adds_tags]) + result += " ".join( + [ + '' + a + "" + for a in self.adds_tags + ] + ) if self.removes_tags: - result += ' '.join(['' + a + '' for a in self.removes_tags]) + result += " ".join( + [ + '' + a + "" + for a in self.removes_tags + ] + ) result += "" return mark_safe(result) @@ -1087,6 +1521,8 @@ class ModerationAnswer(models.Model): self.removes_tags = [] if event.tags: - event.tags = list((set(event.tags) | set(self.adds_tags)) - set(self.removes_tags)) + event.tags = list( + (set(event.tags) | set(self.adds_tags)) - set(self.removes_tags) + ) else: event.tags = self.adds_tags diff --git a/src/agenda_culturel/settings/base.py b/src/agenda_culturel/settings/base.py index cd20fcf..d9800c6 100644 --- a/src/agenda_culturel/settings/base.py +++ b/src/agenda_culturel/settings/base.py @@ -1,4 +1,4 @@ -from os import getenv as os_getenv, path as os_path # noqa +from os import getenv as os_getenv, path as os_path # noqa from pathlib import Path from django.utils.translation import gettext_lazy as _ @@ -18,7 +18,7 @@ ALLOWED_HOSTS = os_getenv("ALLOWED_HOSTS", "localhost").split(",") if DEBUG: CSRF_TRUSTED_ORIGINS = os_getenv("CSRF_TRUSTED_ORIGINS", "http://localhost").split( - "," + "," ) CORS_ORIGIN_ALLOW_ALL = True else: @@ -41,21 +41,21 @@ INSTALLED_APPS = [ "corsheaders", "agenda_culturel", "colorfield", - 'django_extensions', - 'django_better_admin_arrayfield', - 'django_filters', - 'compressor', - 'ckeditor', - 'recurrence', - 'location_field.apps.DefaultConfig', - 'django.contrib.postgres', + "django_extensions", + "django_better_admin_arrayfield", + "django_filters", + "compressor", + "ckeditor", + "recurrence", + "location_field.apps.DefaultConfig", + "django.contrib.postgres", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", - 'django.middleware.locale.LocaleMiddleware', + "django.middleware.locale.LocaleMiddleware", "corsheaders.middleware.CorsMiddleware", # CorsMiddleware should be placed as high as possible, "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -65,10 +65,10 @@ MIDDLEWARE = [ ] CKEDITOR_CONFIGS = { - 'default': { - 'toolbar': 'full', - 'removePlugins': 'stylesheetparser', - 'allowedContent': True, + "default": { + "toolbar": "full", + "removePlugins": "stylesheetparser", + "allowedContent": True, }, } @@ -136,8 +136,8 @@ USE_I18N = True USE_TZ = True LANGUAGES = ( - ('en-us', _('English')), - ('fr', _('French')), + ("en-us", _("English")), + ("fr", _("French")), ) @@ -154,9 +154,9 @@ MEDIA_URL = "media/" MEDIA_ROOT = os_path.join(BASE_DIR, "media") STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'compressor.finders.CompressorFinder' + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "compressor.finders.CompressorFinder", ] # Default primary key field type @@ -185,9 +185,7 @@ CELERY_BROKER_URL = REDIS_URL CELERY_RESULT_BACKEND = REDIS_URL # SCSS -COMPRESS_PRECOMPILERS = ( - ('text/x-scss', 'django_libsass.SassCompiler'), -) +COMPRESS_PRECOMPILERS = (("text/x-scss", "django_libsass.SassCompiler"),) # EMAIL settings @@ -213,7 +211,7 @@ RECURRENCE_I18N_URL = "javascript-catalog" # location field LOCATION_FIELD = { - 'map.provider': 'openstreetmap', - 'provider.openstreetmap.max_zoom': 18, - 'search.provider': 'addok', + "map.provider": "openstreetmap", + "provider.openstreetmap.max_zoom": 18, + "search.provider": "addok", } diff --git a/src/agenda_culturel/settings/production.py b/src/agenda_culturel/settings/production.py index 5befa23..d260a50 100644 --- a/src/agenda_culturel/settings/production.py +++ b/src/agenda_culturel/settings/production.py @@ -1,3 +1,3 @@ COMPRESS_OFFLINE = False -LIBSASS_OUTPUT_STYLE = 'compressed' -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' +LIBSASS_OUTPUT_STYLE = "compressed" +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" diff --git a/src/agenda_culturel/templatetags/cat_extra.py b/src/agenda_culturel/templatetags/cat_extra.py index e0b1373..0222844 100644 --- a/src/agenda_culturel/templatetags/cat_extra.py +++ b/src/agenda_culturel/templatetags/cat_extra.py @@ -10,24 +10,28 @@ register = template.Library() def html_to_rgb(hex_color): - """ takes a color like #87c95f and produces a desaturate color """ + """takes a color like #87c95f and produces a desaturate color""" if len(hex_color) != 7: - raise Exception("Passed %s into color_variant(), needs to be in #87c95f format." % hex_color) + raise Exception( + "Passed %s into color_variant(), needs to be in #87c95f format." % hex_color + ) - rgb_hex = [hex_color[x:x + 2] for x in [1, 3, 5]] + rgb_hex = [hex_color[x : x + 2] for x in [1, 3, 5]] rgb_in = [int(hex_value, 16) for hex_value in rgb_hex] return [x / 255 for x in rgb_in] + def rgb_to_html(rgb): - new_rgb_int = [min([255, max([0, int(i * 255)])]) for i in rgb] # make sure new values are between 0 and 255 - + new_rgb_int = [ + min([255, max([0, int(i * 255)])]) for i in rgb + ] # make sure new values are between 0 and 255 + # hex() produces "0x88", we want just "88" return "#" + "".join([("0" + hex(i)[2:])[-2:] for i in new_rgb_int]) def get_relative_luminance(hex_color): - rgb = html_to_rgb(hex_color) R = rgb[0] / 12.92 if rgb[0] <= 0.04045 else ((rgb[0] + 0.055) / 1.055) ** 2.4 G = rgb[1] / 12.92 if rgb[1] <= 0.04045 else ((rgb[1] + 0.055) / 1.055) ** 2.4 @@ -35,7 +39,8 @@ def get_relative_luminance(hex_color): return 0.2126 * R + 0.7152 * G + 0.0722 * B -def adjust_lightness_saturation(hex_color, shift_lightness = 0.0, scale_saturation=1): + +def adjust_lightness_saturation(hex_color, shift_lightness=0.0, scale_saturation=1): rgb = html_to_rgb(hex_color) h, l, s = colorsys.rgb_to_hls(*rgb) @@ -43,16 +48,18 @@ def adjust_lightness_saturation(hex_color, shift_lightness = 0.0, scale_saturati l += shift_lightness s *= scale_saturation - r, g, b = colorsys.hls_to_rgb(h, l, s) + r, g, b = colorsys.hls_to_rgb(h, l, s) return rgb_to_html([r, g, b]) -def adjust_color(color, alpha = 1): + +def adjust_color(color, alpha=1): return color + ("0" + hex(int(alpha * 255))[2:])[-2:] -def background_color_adjust_color(color, alpha = 1): + +def background_color_adjust_color(color, alpha=1): result = " background-color: " + adjust_color(color, alpha) + ";" - if get_relative_luminance(color) < .5: + if get_relative_luminance(color) < 0.5: result += " color: #fff;" else: result += " color: #000;" @@ -62,20 +69,27 @@ def background_color_adjust_color(color, alpha = 1): @register.simple_tag def css_categories(): result = '' + result += "" return mark_safe(result) + @register.filter def small_cat(category, url=None, contrast=True, selected=True, recurrence=False): - name = Category.default_name if category is None else category.name css_class = Category.default_css_class if category is None else category.css_class() @@ -114,16 +133,50 @@ def small_cat(category, url=None, contrast=True, selected=True, recurrence=False class_selected = " selected" if selected else "" class_recurrence = " recurrent" if recurrence else "" - content = ('' + picto_from_name("repeat", name + ' [récurrent]') + '') if recurrence else ('') + content = ( + ( + '' + + picto_from_name("repeat", name + " [récurrent]") + + "" + ) + if recurrence + else ('') + ) if url is None: - return mark_safe('' + content + " " + name + "") + return mark_safe( + '' + + content + + " " + + name + + "" + ) else: - return mark_safe('' + content + " " + name + "") + return mark_safe( + '' + + content + + " " + + name + + "" + ) + @register.filter def small_cat_no_selected(category, url=None): return small_cat(category, url=url, selected=False) + @register.filter def small_cat_recurrent(category, recurrence=False): return small_cat(category, url=None, selected=True, recurrence=recurrence) @@ -139,9 +192,17 @@ def circle_cat(category, recurrence=False): n = category.name if recurrence: - return mark_safe('' + picto_from_name("repeat", n + ' [récurrent]') + "") + return mark_safe( + '' + + picto_from_name("repeat", n + " [récurrent]") + + "" + ) else: - return mark_safe('') + return mark_safe( + '' + ) @register.simple_tag @@ -149,6 +210,23 @@ def show_legend(filter): current_url = filter.get_url_without_filters() cats = Category.objects.all() if filter.is_active(only_categories=True): - return mark_safe(" ".join([small_cat(c, current_url + "?category=" + str(c.pk) if not filter.is_selected(c) else None, contrast=filter.is_selected(c)) for c in cats])) + return mark_safe( + " ".join( + [ + small_cat( + c, + current_url + "?category=" + str(c.pk) + if not filter.is_selected(c) + else None, + contrast=filter.is_selected(c), + ) + for c in cats + ] + ) + ) else: - return mark_safe(" ".join([small_cat(c, current_url + "?category=" + str(c.pk)) for c in cats])) \ No newline at end of file + return mark_safe( + " ".join( + [small_cat(c, current_url + "?category=" + str(c.pk)) for c in cats] + ) + ) diff --git a/src/agenda_culturel/templatetags/contactmessages_extra.py b/src/agenda_culturel/templatetags/contactmessages_extra.py index 3535987..c556d79 100644 --- a/src/agenda_culturel/templatetags/contactmessages_extra.py +++ b/src/agenda_culturel/templatetags/contactmessages_extra.py @@ -15,6 +15,20 @@ register = template.Library() def show_badge_contactmessages(placement="top"): nb_open = ContactMessage.nb_open_contactmessages() if nb_open != 0: - return mark_safe('' + picto_from_name("mail") + " " + str(nb_open) + '') + return mark_safe( + '' + + picto_from_name("mail") + + " " + + str(nb_open) + + "" + ) else: - return "" \ No newline at end of file + return "" diff --git a/src/agenda_culturel/templatetags/duplicated_extra.py b/src/agenda_culturel/templatetags/duplicated_extra.py index e1d3b19..fe26708 100644 --- a/src/agenda_culturel/templatetags/duplicated_extra.py +++ b/src/agenda_culturel/templatetags/duplicated_extra.py @@ -10,6 +10,7 @@ from .utils_extra import picto_from_name register = template.Library() + @register.simple_tag def show_badge_duplicated(placement="top"): duplicated = DuplicatedEvents.objects.all() @@ -22,6 +23,20 @@ def show_badge_duplicated(placement="top"): d.delete() if nb_duplicated != 0: - return mark_safe('' + picto_from_name("copy") + " " + str(nb_duplicated) + '') + return mark_safe( + '' + + picto_from_name("copy") + + " " + + str(nb_duplicated) + + "" + ) else: - return "" \ No newline at end of file + return "" diff --git a/src/agenda_culturel/templatetags/event_extra.py b/src/agenda_culturel/templatetags/event_extra.py index d4ff22c..93bebe8 100644 --- a/src/agenda_culturel/templatetags/event_extra.py +++ b/src/agenda_culturel/templatetags/event_extra.py @@ -11,26 +11,36 @@ from .utils_extra import picto_from_name register = template.Library() + @register.filter def in_date(event, date): - return event.filter((Q(start_day__lte=date) & Q(end_day__gte=date)) | (Q(end_day=None) & Q(start_day=date))) + return event.filter( + (Q(start_day__lte=date) & Q(end_day__gte=date)) + | (Q(end_day=None) & Q(start_day=date)) + ) + @register.filter def can_show_start_time(event, day=None): if not day is None and day == event.start_day: - return True + return True return event.start_time and (not event.end_day or event.end_day == event.start_day) + @register.filter def can_show_end_time(event, day=None): if not day is None and day == event.end_day and event.start_day != event.end_day: - return True + return True return False @register.filter def need_complete_display(event, display_full=True): - return event.end_day and event.end_day != event.start_day and (event.start_time or event.end_time or display_full) + return ( + event.end_day + and event.end_day != event.start_day + and (event.start_time or event.end_time or display_full) + ) @register.filter @@ -47,15 +57,44 @@ def picto_status(event): def show_badges_events(placement="top"): nb_drafts = Event.nb_draft_events() if nb_drafts != 0: - return mark_safe('' + picto_from_name("calendar") + " " + str(nb_drafts) + '') + return mark_safe( + '' + + picto_from_name("calendar") + + " " + + str(nb_drafts) + + "" + ) else: return "" + @register.simple_tag def show_badge_unknown_places(placement="top"): nb_unknown = Event.objects.filter(exact_location__isnull=True).count() if nb_unknown != 0: - return mark_safe('' + picto_from_name("map-pin") + " " + str(nb_unknown) + '') + return mark_safe( + '' + + picto_from_name("map-pin") + + " " + + str(nb_unknown) + + "" + ) else: return "" @@ -64,6 +103,7 @@ def show_badge_unknown_places(placement="top"): def event_field_verbose_name(the_field): return Event._meta.get_field(the_field).verbose_name + @register.simple_tag def field_to_html(field, key): if field is None: @@ -71,13 +111,17 @@ def field_to_html(field, key): elif key == "description": return urlize(mark_safe(linebreaks(field))) elif key == "reference_urls": - return mark_safe("") + return mark_safe( + "" + ) elif key == "image": - return mark_safe('' + field + '') + return mark_safe('' + field + "") elif key == "local_image": if field: return mark_safe('') else: return "-" else: - return field \ No newline at end of file + return field diff --git a/src/agenda_culturel/templatetags/rimports_extra.py b/src/agenda_culturel/templatetags/rimports_extra.py index 3ece1b5..fa8aa59 100644 --- a/src/agenda_culturel/templatetags/rimports_extra.py +++ b/src/agenda_culturel/templatetags/rimports_extra.py @@ -11,13 +11,37 @@ from .utils_extra import picto_from_name register = template.Library() + @register.simple_tag def show_badge_failed_rimports(placement="top"): - newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by("-created_date") - nb_failed = RecurrentImport.objects.annotate(last_run_status=Subquery(newest.values("status")[:1])).filter(last_run_status=BatchImportation.STATUS.FAILED).count() - + newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by( + "-created_date" + ) + nb_failed = ( + RecurrentImport.objects.annotate( + last_run_status=Subquery(newest.values("status")[:1]) + ) + .filter(last_run_status=BatchImportation.STATUS.FAILED) + .count() + ) if nb_failed != 0: - return mark_safe('' + picto_from_name("alert-triangle") + " " + str(nb_failed) + '') + return mark_safe( + '' + + picto_from_name("alert-triangle") + + " " + + str(nb_failed) + + "" + ) else: - return "" \ No newline at end of file + return "" diff --git a/src/agenda_culturel/templatetags/static_content_extra.py b/src/agenda_culturel/templatetags/static_content_extra.py index 256f43c..dd90b15 100644 --- a/src/agenda_culturel/templatetags/static_content_extra.py +++ b/src/agenda_culturel/templatetags/static_content_extra.py @@ -6,6 +6,7 @@ from django.db.models import Q register = template.Library() + @register.simple_tag def get_static_content_by_name(name): result = StaticContent.objects.filter(name=name) @@ -14,7 +15,8 @@ def get_static_content_by_name(name): else: return result[0] + @register.simple_tag def concat_all(*args): """concatenate all args""" - return ''.join(map(str, args)) \ No newline at end of file + return "".join(map(str, args)) diff --git a/src/agenda_culturel/templatetags/tag_extra.py b/src/agenda_culturel/templatetags/tag_extra.py index 0ad73eb..d5bca26 100644 --- a/src/agenda_culturel/templatetags/tag_extra.py +++ b/src/agenda_culturel/templatetags/tag_extra.py @@ -4,15 +4,30 @@ from django.urls import reverse_lazy register = template.Library() + @register.filter def tag_button(tag, link=False, strike=False): strike_class = " strike" if strike else "" if link: - return mark_safe('' + tag + '') + return mark_safe( + '' + + tag + + "" + ) else: - return mark_safe('' + tag + '') + return mark_safe( + '' + + tag + + "" + ) @register.filter def tag_button_strike(tag, link=False): - return tag_button(tag, link, strike=True) \ No newline at end of file + return tag_button(tag, link, strike=True) diff --git a/src/agenda_culturel/templatetags/utils_extra.py b/src/agenda_culturel/templatetags/utils_extra.py index 2b808be..b7965b5 100644 --- a/src/agenda_culturel/templatetags/utils_extra.py +++ b/src/agenda_culturel/templatetags/utils_extra.py @@ -19,7 +19,7 @@ def hostname(url): @register.filter def add_de(txt): - return ("d'" if txt[0].lower() in ['a', 'e', 'i', 'o', 'u', 'y'] else "de ") + txt + return ("d'" if txt[0].lower() in ["a", "e", "i", "o", "u", "y"] else "de ") + txt @register.filter @@ -39,7 +39,7 @@ def first_day_of_this_week(d): @register.filter def last_day_of_this_week(d): - return date.fromisocalendar(d.year, week(d), 7) + return date.fromisocalendar(d.year, week(d), 7) @register.filter @@ -57,22 +57,33 @@ def calendar_classes(d, fixed_style): @register.filter def url_day(d): - return reverse_lazy("day_view", kwargs={"year": d.year, "month": d.month, "day": d.day}) + return reverse_lazy( + "day_view", kwargs={"year": d.year, "month": d.month, "day": d.day} + ) + @register.simple_tag def picto_from_name(name, datatooltip=""): - result = '' + \ - '' + \ - '' + result = ( + '' + + '' + + "" + ) if datatooltip != "": - result = '' + result + '' + result = '' + result + "" return mark_safe(result) + @register.filter def int_to_abc(d): return auc[int(d)] + @register.filter(is_safe=True) @stringfilter def truncatechars_middle(value, arg): @@ -83,17 +94,19 @@ def truncatechars_middle(value, arg): if len(value) <= ln: return value else: - return '{}...{}'.format(value[:ln//2], value[-((ln+1)//2):]) + return "{}...{}".format(value[: ln // 2], value[-((ln + 1) // 2) :]) + @register.filter def frdate(d): - return ('!' + d).replace(" 1 ", " 1er ").replace("!1 ", "!1er ")[1:] + return ("!" + d).replace(" 1 ", " 1er ").replace("!1 ", "!1er ")[1:] + @register.filter def get_item(dictionary, key): return dictionary.get(key) + @register.filter def remove_id_prefix(value): return int(value.replace("id_", "")) - \ No newline at end of file diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index f897e87..4679fcd 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -11,34 +11,58 @@ from .views import * urlpatterns = [ path("", home, name="home"), - path("semaine///", week_view, name='week_view'), - path("mois///", month_view, name='month_view'), - path("jour////", day_view, name='day_view'), + path("semaine///", week_view, name="week_view"), + path("mois///", month_view, name="month_view"), + path("jour////", day_view, name="day_view"), path("aujourdhui/", day_view, name="aujourdhui"), path("cette-semaine/", week_view, name="cette_semaine"), path("ce-mois-ci", month_view, name="ce_mois_ci"), - path("tag//", view_tag, name='view_tag'), - path("tags/", tag_list, name='view_all_tags'), - path("moderation/", moderation, name='moderation'), - path("event////-", EventDetailView.as_view(), name="view_event"), + path("tag//", view_tag, name="view_tag"), + path("tags/", tag_list, name="view_all_tags"), + path("moderation/", moderation, name="moderation"), + path( + "event////-", + EventDetailView.as_view(), + name="view_event", + ), path("event//edit", EventUpdateView.as_view(), name="edit_event"), - path("event//change-status/", change_status_event, name="change_status_event"), + path( + "event//change-status/", + change_status_event, + name="change_status_event", + ), path("event//delete", EventDeleteView.as_view(), name="delete_event"), - path("event/////set_duplicate", set_duplicate, name="set_duplicate"), + path( + "event/////set_duplicate", + set_duplicate, + name="set_duplicate", + ), path("event//moderate", EventModerateView.as_view(), name="moderate_event"), path("ajouter", import_from_url, name="add_event"), path("admin/", admin.site.urls), - path('accounts/', include('django.contrib.auth.urls')), + path("accounts/", include("django.contrib.auth.urls")), path("test_app/", include("test_app.urls")), - path("static-content/create", StaticContentCreateView.as_view(), name="create_static_content"), - path("static-content//edit", StaticContentUpdateView.as_view(), name="edit_static_content"), - path('rechercher', event_search, name='event_search'), - path('rechercher/complet/', event_search_full, name='event_search_full'), - path('mentions-legales', mentions_legales, name='mentions_legales'), - path('a-propos', about, name='about'), - path('contact', ContactMessageCreateView.as_view(), name='contact'), - path('contactmessages', contactmessages, name='contactmessages'), - path('contactmessage/', ContactMessageUpdateView.as_view(), name='contactmessage'), + path( + "static-content/create", + StaticContentCreateView.as_view(), + name="create_static_content", + ), + path( + "static-content//edit", + StaticContentUpdateView.as_view(), + name="edit_static_content", + ), + path("rechercher", event_search, name="event_search"), + path("rechercher/complet/", event_search_full, name="event_search_full"), + path("mentions-legales", mentions_legales, name="mentions_legales"), + path("a-propos", about, name="about"), + path("contact", ContactMessageCreateView.as_view(), name="contact"), + path("contactmessages", contactmessages, name="contactmessages"), + path( + "contactmessage/", + ContactMessageUpdateView.as_view(), + name="contactmessage", + ), path("imports/", imports, name="imports"), path("imports/add", add_import, name="add_import"), path("imports//cancel", cancel_import, name="cancel_import"), @@ -46,26 +70,72 @@ urlpatterns = [ path("rimports/run", run_all_rimports, name="run_all_rimports"), path("rimports/add", RecurrentImportCreateView.as_view(), name="add_rimport"), path("rimports//view", view_rimport, name="view_rimport"), - path("rimports//edit", RecurrentImportUpdateView.as_view(), name="edit_rimport"), - path("rimports//delete", RecurrentImportDeleteView.as_view(), name="delete_rimport"), + path( + "rimports//edit", + RecurrentImportUpdateView.as_view(), + name="edit_rimport", + ), + path( + "rimports//delete", + RecurrentImportDeleteView.as_view(), + name="delete_rimport", + ), path("rimports//run", run_rimport, name="run_rimport"), path("catrules/", categorisation_rules, name="categorisation_rules"), path("catrules/add", CategorisationRuleCreateView.as_view(), name="add_catrule"), - path("catrules//edit", CategorisationRuleUpdateView.as_view(), name="edit_catrule"), - path("catrules//delete", CategorisationRuleDeleteView.as_view(), name="delete_catrule"), + path( + "catrules//edit", + CategorisationRuleUpdateView.as_view(), + name="edit_catrule", + ), + path( + "catrules//delete", + CategorisationRuleDeleteView.as_view(), + name="delete_catrule", + ), path("catrules/apply", apply_categorisation_rules, name="apply_catrules"), path("duplicates/", duplicates, name="duplicates"), - path("duplicates/", DuplicatedEventsDetailView.as_view(), name="view_duplicate"), + path( + "duplicates/", + DuplicatedEventsDetailView.as_view(), + name="view_duplicate", + ), path("duplicates//fix", fix_duplicate, name="fix_duplicate"), path("duplicates//merge", merge_duplicate, name="merge_duplicate"), path("mquestions/", ModerationQuestionListView.as_view(), name="view_mquestions"), - path("mquestions/add", ModerationQuestionCreateView.as_view(), name="add_mquestion"), - path("mquestions//", ModerationQuestionDetailView.as_view(), name="view_mquestion"), - path("mquestions//edit", ModerationQuestionUpdateView.as_view(), name="edit_mquestion"), - path("mquestions//delete", ModerationQuestionDeleteView.as_view(), name="delete_mquestion"), - path("mquestions//answers/add", ModerationAnswerCreateView.as_view(), name="add_manswer"), - path("mquestions//answers//edit", ModerationAnswerUpdateView.as_view(), name="edit_manswer"), - path("mquestions//answers//delete", ModerationAnswerDeleteView.as_view(), name="delete_manswer"), + path( + "mquestions/add", ModerationQuestionCreateView.as_view(), name="add_mquestion" + ), + path( + "mquestions//", + ModerationQuestionDetailView.as_view(), + name="view_mquestion", + ), + path( + "mquestions//edit", + ModerationQuestionUpdateView.as_view(), + name="edit_mquestion", + ), + path( + "mquestions//delete", + ModerationQuestionDeleteView.as_view(), + name="delete_mquestion", + ), + path( + "mquestions//answers/add", + ModerationAnswerCreateView.as_view(), + name="add_manswer", + ), + path( + "mquestions//answers//edit", + ModerationAnswerUpdateView.as_view(), + name="edit_manswer", + ), + path( + "mquestions//answers//delete", + ModerationAnswerDeleteView.as_view(), + name="delete_manswer", + ), path("404/", page_not_found, name="page_not_found"), path("500/", internal_server_error, name="internal_server_error"), path("place/", PlaceDetailView.as_view(), name="view_place"), @@ -73,11 +143,22 @@ urlpatterns = [ path("place//delete", PlaceDeleteView.as_view(), name="delete_place"), path("places/", PlaceListView.as_view(), name="view_places"), path("places/add", PlaceCreateView.as_view(), name="add_place"), - path("places/add/", PlaceFromEventCreateView.as_view(), name="add_place_from_event"), - path("events/unknown-places", UnknownPlacesListView.as_view(), name="view_unknown_places"), + path( + "places/add/", + PlaceFromEventCreateView.as_view(), + name="add_place_from_event", + ), + path( + "events/unknown-places", + UnknownPlacesListView.as_view(), + name="view_unknown_places", + ), path("events/unknown-places/fix", fix_unknown_places, name="fix_unknown_places"), - path("event//addplace", UnknownPlaceAddView.as_view(), name="add_place_to_event"), - + path( + "event//addplace", + UnknownPlaceAddView.as_view(), + name="add_place_to_event", + ), ] if settings.DEBUG: @@ -87,11 +168,15 @@ if settings.DEBUG: # If you already have a js_info_dict dictionary, just add # 'recurrence' to the existing 'packages' tuple. js_info_dict = { - 'packages': ('recurrence', ), + "packages": ("recurrence",), } # jsi18n can be anything you like here -urlpatterns += [ path('jsi18n.js', JavaScriptCatalog.as_view(packages=['recurrence']), name='jsi18n'), ] +urlpatterns += [ + path( + "jsi18n.js", JavaScriptCatalog.as_view(packages=["recurrence"]), name="jsi18n" + ), +] -handler404 = 'agenda_culturel.views.page_not_found' -handler500 = 'agenda_culturel.views.internal_server_error' \ No newline at end of file +handler404 = "agenda_culturel.views.page_not_found" +handler500 = "agenda_culturel.views.internal_server_error" diff --git a/src/agenda_culturel/views.py b/src/agenda_culturel/views.py index 5005649..914ef53 100644 --- a/src/agenda_culturel/views.py +++ b/src/agenda_culturel/views.py @@ -1,7 +1,11 @@ from django.shortcuts import render, get_object_or_404 from django.views.generic import ListView, DetailView, FormView from django.views.generic.edit import CreateView, UpdateView, DeleteView -from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin, PermissionRequiredMixin +from django.contrib.auth.mixins import ( + LoginRequiredMixin, + UserPassesTestMixin, + PermissionRequiredMixin, +) from django.http import QueryDict from django import forms from django.contrib.postgres.search import SearchQuery, SearchHeadline @@ -12,9 +16,37 @@ from django.urls import reverse import urllib from collections import Counter -from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates, RecurrentImportForm, CategorisationRuleImportForm, ModerationQuestionForm, ModerationAnswerForm, ModerateForm, CategorisationForm, EventAddPlaceForm, PlaceForm +from .forms import ( + EventSubmissionForm, + EventForm, + BatchImportationForm, + FixDuplicates, + SelectEventInList, + MergeDuplicates, + RecurrentImportForm, + CategorisationRuleImportForm, + ModerationQuestionForm, + ModerationAnswerForm, + ModerateForm, + CategorisationForm, + EventAddPlaceForm, + PlaceForm, +) -from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents, RecurrentImport, CategorisationRule, remove_accents, ModerationQuestion, ModerationAnswer, Place +from .models import ( + Event, + Category, + StaticContent, + ContactMessage, + BatchImportation, + DuplicatedEvents, + RecurrentImport, + CategorisationRule, + remove_accents, + ModerationQuestion, + ModerationAnswer, + Place, +) from django.utils import timezone from enum import StrEnum from datetime import date, timedelta @@ -36,10 +68,16 @@ from .import_tasks.importer import URL2Events from .import_tasks.extractor import Extractor from .import_tasks.downloader import ChromiumHeadlessDownloader -from .celery import app as celery_app, import_events_from_json, run_recurrent_import, run_all_recurrent_imports +from .celery import ( + app as celery_app, + import_events_from_json, + run_recurrent_import, + run_all_recurrent_imports, +) import unicodedata import logging + logger = logging.getLogger(__name__) @@ -47,16 +85,14 @@ logger = logging.getLogger(__name__) # # Useful for translation to_be_translated = [ - _("Recurrent import name"), - _("Add another"), + _("Recurrent import name"), + _("Add another"), _("Browse..."), _("Naviguer..."), - _("No file selected.") + _("No file selected."), ] - - def get_event_qs(request): if request.user.is_authenticated: return Event.objects.filter() @@ -65,58 +101,73 @@ def get_event_qs(request): def page_not_found(request, exception=None): - return render(request, 'page-erreur.html', status=404, context={"error": 404}) + return render(request, "page-erreur.html", status=404, context={"error": 404}) def internal_server_error(request): - return render(request, 'page-erreur.html', status=500, context={"error": 500}) + return render(request, "page-erreur.html", status=500, context={"error": 500}) class CategoryCheckboxSelectMultiple(forms.CheckboxSelectMultiple): - template_name = 'agenda_culturel/forms/category-checkbox.html' - option_template_name = 'agenda_culturel/forms/checkbox-option.html' + template_name = "agenda_culturel/forms/category-checkbox.html" + option_template_name = "agenda_culturel/forms/checkbox-option.html" + class TagCheckboxSelectMultiple(forms.CheckboxSelectMultiple): - template_name = 'agenda_culturel/forms/tag-checkbox.html' - option_template_name = 'agenda_culturel/forms/checkbox-option.html' + template_name = "agenda_culturel/forms/tag-checkbox.html" + option_template_name = "agenda_culturel/forms/checkbox-option.html" class EventFilter(django_filters.FilterSet): - RECURRENT_CHOICES = [("remove_recurrent", "Masquer les événements récurrents"), ("only_recurrent", "Montrer uniquement les événements récurrents")] + RECURRENT_CHOICES = [ + ("remove_recurrent", "Masquer les événements récurrents"), + ("only_recurrent", "Montrer uniquement les événements récurrents"), + ] - exclude_tags = django_filters.MultipleChoiceFilter(label="Exclure les étiquettes", + exclude_tags = django_filters.MultipleChoiceFilter( + label="Exclure les étiquettes", choices=[(t, t) for t in Event.get_all_tags()], - lookup_expr='icontains', + lookup_expr="icontains", field_name="tags", exclude=True, - widget=TagCheckboxSelectMultiple) + widget=TagCheckboxSelectMultiple, + ) - tags = django_filters.MultipleChoiceFilter(label="Filtrer par étiquettes", + tags = django_filters.MultipleChoiceFilter( + label="Filtrer par étiquettes", choices=[(t, t) for t in Event.get_all_tags()], - lookup_expr='icontains', - field_name="tags", - widget=TagCheckboxSelectMultiple) + lookup_expr="icontains", + field_name="tags", + widget=TagCheckboxSelectMultiple, + ) - recurrences = django_filters.ChoiceFilter(label="Filtrer par récurrence", + recurrences = django_filters.ChoiceFilter( + label="Filtrer par récurrence", choices=RECURRENT_CHOICES, - method="filter_recurrences") + method="filter_recurrences", + ) - category = django_filters.ModelMultipleChoiceFilter(label="Filtrer par catégories", - field_name="category__id", - to_field_name='id', - queryset=Category.objects.all(), - widget=CategoryCheckboxSelectMultiple) + category = django_filters.ModelMultipleChoiceFilter( + label="Filtrer par catégories", + field_name="category__id", + to_field_name="id", + queryset=Category.objects.all(), + widget=CategoryCheckboxSelectMultiple, + ) - city = django_filters.MultipleChoiceFilter(label="Filtrer par ville", - field_name='exact_location__city', + city = django_filters.MultipleChoiceFilter( + label="Filtrer par ville", + field_name="exact_location__city", choices=[(c, c) for c in Place.get_all_cities()], - widget=forms.CheckboxSelectMultiple) + widget=forms.CheckboxSelectMultiple, + ) - status = django_filters.MultipleChoiceFilter(label="Filtrer par status", + status = django_filters.MultipleChoiceFilter( + label="Filtrer par status", choices=Event.STATUS.choices, - field_name="status", - widget=forms.CheckboxSelectMultiple) - + field_name="status", + widget=forms.CheckboxSelectMultiple, + ) class Meta: model = Event @@ -129,12 +180,12 @@ class EventFilter(django_filters.FilterSet): def filter_recurrences(self, queryset, name, value): # construct the full lookup expression - lookup = '__'.join([name, 'isnull']) + lookup = "__".join([name, "isnull"]) return queryset.filter(**{lookup: value == "remove_recurrent"}) def get_url(self): if isinstance(self.form.data, QueryDict): - return self.form.data.urlencode() + return self.form.data.urlencode() else: print(self.form.data) return "" @@ -159,9 +210,12 @@ class EventFilter(django_filters.FilterSet): def get_status_names(self): if "status" in self.form.cleaned_data: - return [dict(Event.STATUS.choices)[s] for s in self.form.cleaned_data["status"]] + return [ + dict(Event.STATUS.choices)[s] for s in self.form.cleaned_data["status"] + ] else: return [] + def get_recurrence_filtering(self): if "recurrences" in self.form.cleaned_data: d = dict(self.RECURRENT_CHOICES) @@ -177,28 +231,48 @@ class EventFilter(django_filters.FilterSet): if only_categories: return len(self.form.cleaned_data["category"]) != 0 else: - if "status" in self.form.cleaned_data and len(self.form.cleaned_data["status"]) != 0: + if ( + "status" in self.form.cleaned_data + and len(self.form.cleaned_data["status"]) != 0 + ): return True - return len(self.form.cleaned_data["category"]) != 0 or len(self.form.cleaned_data["tags"]) != 0 or len(self.form.cleaned_data["exclude_tags"]) != 0 or len(self.form.cleaned_data["recurrences"]) != 0 or len(self.form.cleaned_data["city"]) != 0 + return ( + len(self.form.cleaned_data["category"]) != 0 + or len(self.form.cleaned_data["tags"]) != 0 + or len(self.form.cleaned_data["exclude_tags"]) != 0 + or len(self.form.cleaned_data["recurrences"]) != 0 + or len(self.form.cleaned_data["city"]) != 0 + ) def is_selected(self, cat): return cat in self.form.cleaned_data["category"] def mentions_legales(request): - context = { "title": "Mentions légales", "static_content": "mentions_legales", "url_path": reverse_lazy("mentions_legales") } - return render(request, 'agenda_culturel/page-single.html', context) + context = { + "title": "Mentions légales", + "static_content": "mentions_legales", + "url_path": reverse_lazy("mentions_legales"), + } + return render(request, "agenda_culturel/page-single.html", context) + def about(request): rimports = RecurrentImport.objects.order_by("name__unaccent").all() - context = { "title": "À propos", "static_content": "about", "url_path": reverse_lazy("about"), "rimports": rimports } - return render(request, 'agenda_culturel/page-rimports-list.html', context) + context = { + "title": "À propos", + "static_content": "about", + "url_path": reverse_lazy("about"), + "rimports": rimports, + } + return render(request, "agenda_culturel/page-rimports-list.html", context) def home(request): return week_view(request, home=True) -def month_view(request, year = None, month = None): + +def month_view(request, year=None, month=None): now = date.today() if year is None: year = now.year @@ -207,10 +281,14 @@ def month_view(request, year = None, month = None): filter = EventFilter(request.GET, queryset=get_event_qs(request), request=request) cmonth = CalendarMonth(year, month, filter) - - context = {"year": year, "month": cmonth.get_month_name(), "calendar": cmonth, "filter": filter } - return render(request, 'agenda_culturel/page-month.html', context) + context = { + "year": year, + "month": cmonth.get_month_name(), + "calendar": cmonth, + "filter": filter, + } + return render(request, "agenda_culturel/page-month.html", context) def week_view(request, year=None, week=None, home=False): @@ -223,13 +301,13 @@ def week_view(request, year=None, week=None, home=False): filter = EventFilter(request.GET, queryset=get_event_qs(request), request=request) cweek = CalendarWeek(year, week, filter) - context = {"year": year, "week": week, "calendar": cweek, "filter": filter } + context = {"year": year, "week": week, "calendar": cweek, "filter": filter} if home: context["home"] = 1 - return render(request, 'agenda_culturel/page-week.html', context) + return render(request, "agenda_culturel/page-week.html", context) -def day_view(request, year = None, month = None, day = None): +def day_view(request, year=None, month=None, day=None): now = date.today() if year is None: year = now.year @@ -239,66 +317,83 @@ def day_view(request, year = None, month = None, day = None): day = now.day day = date(year, month, day) - + filter = EventFilter(request.GET, get_event_qs(request), request=request) cday = CalendarDay(day, filter) - categories = Counter([e.category if e.category is not None else Category.get_default_category() for e in cday.get_events()]) + categories = Counter( + [ + e.category if e.category is not None else Category.get_default_category() + for e in cday.get_events() + ] + ) categories = [(k, v) for k, v in categories.items()] categories.sort(key=lambda k: -k[1]) - context = {"day": day, "events": cday.get_events(), "filter": filter, "categories": categories} - return render(request, 'agenda_culturel/page-day.html', context) + context = { + "day": day, + "events": cday.get_events(), + "filter": filter, + "categories": categories, + } + return render(request, "agenda_culturel/page-day.html", context) def view_tag(request, t): - events = Event.objects.filter(tags__contains=[t]).order_by("start_day", "start_time") + events = Event.objects.filter(tags__contains=[t]).order_by( + "start_day", "start_time" + ) context = {"tag": t, "events": events} - return render(request, 'agenda_culturel/tag.html', context) + return render(request, "agenda_culturel/tag.html", context) def tag_list(request): - - tags = Event.get_all_tags() + tags = Event.get_all_tags() context = {"tags": sorted(tags, key=lambda x: remove_accents(x).lower())} - return render(request, 'agenda_culturel/tags.html', context) + return render(request, "agenda_culturel/tags.html", context) class StaticContentCreateView(LoginRequiredMixin, CreateView): model = StaticContent - fields = ['text'] - permission_required = ("agenda_culturel.add_staticcontent") + fields = ["text"] + permission_required = "agenda_culturel.add_staticcontent" def form_valid(self, form): - form.instance.name = self.request.GET["name"] - form.instance.url_path = self.request.GET["url_path"] - return super().form_valid(form) + form.instance.name = self.request.GET["name"] + form.instance.url_path = self.request.GET["url_path"] + return super().form_valid(form) -class StaticContentUpdateView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView): +class StaticContentUpdateView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView +): model = StaticContent - permission_required = ("agenda_culturel.change_staticcontent") - fields = ['text'] - success_message = _('The static content has been successfully updated.') + permission_required = "agenda_culturel.change_staticcontent" + fields = ["text"] + success_message = _("The static content has been successfully updated.") -class EventUpdateView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView): +class EventUpdateView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView +): model = Event - permission_required = ("agenda_culturel.change_event") + permission_required = "agenda_culturel.change_event" form_class = EventForm - success_message = _('The event has been successfully modified.') + success_message = _("The event has been successfully modified.") def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['is_authenticated'] = self.request.user.is_authenticated + kwargs["is_authenticated"] = self.request.user.is_authenticated return kwargs -class EventDeleteView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, DeleteView): +class EventDeleteView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, DeleteView +): model = Event - permission_required = ("agenda_culturel.delete_event") - success_url = reverse_lazy('moderation') - success_message = _('The event has been successfully deleted.') + permission_required = "agenda_culturel.delete_event" + success_url = reverse_lazy("moderation") + success_message = _("The event has been successfully deleted.") class EventDetailView(UserPassesTestMixin, DetailView): @@ -306,7 +401,10 @@ class EventDetailView(UserPassesTestMixin, DetailView): template_name = "agenda_culturel/page-event.html" def test_func(self): - return self.request.user.is_authenticated or self.get_object().status == Event.STATUS.PUBLISHED + return ( + self.request.user.is_authenticated + or self.get_object().status == Event.STATUS.PUBLISHED + ) def get_object(self): o = super().get_object() @@ -318,33 +416,38 @@ class EventDetailView(UserPassesTestMixin, DetailView): return obj -class EventModerateView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView): +class EventModerateView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView +): model = Event - permission_required = ("agenda_culturel.change_event", "agenda_culturel.use_moderation_question") - success_message = _('The event has been successfully modified.') + permission_required = ( + "agenda_culturel.change_event", + "agenda_culturel.use_moderation_question", + ) + success_message = _("The event has been successfully modified.") form_class = ModerateForm - template_name = 'agenda_culturel/event_moderation_form.html' + template_name = "agenda_culturel/event_moderation_form.html" def form_valid(self, form): - mas = ModerationAnswer.objects.all() logger.warning("ON valide la forme") for f in form.cleaned_data: - ModerationAnswer.objects.get(pk=form.cleaned_data[f]).apply_answer(form.instance) + ModerationAnswer.objects.get(pk=form.cleaned_data[f]).apply_answer( + form.instance + ) form.instance.moderated_date = timezone.now() return super().form_valid(form) - @login_required(login_url="/accounts/login/") -@permission_required('agenda_culturel.change_event') +@permission_required("agenda_culturel.change_event") def change_status_event(request, pk, status): event = get_object_or_404(Event, pk=pk) - if request.method == 'POST': + if request.method == "POST": event.status = Event.STATUS(status) event.save(update_fields=["status"]) messages.success(request, _("The status has been successfully modified.")) @@ -354,17 +457,22 @@ def change_status_event(request, pk, status): else: return HttpResponseRedirect(reverse_lazy("home")) else: - cancel_url = request.META.get('HTTP_REFERER', '') + cancel_url = request.META.get("HTTP_REFERER", "") if cancel_url == "": cancel_url = reverse_lazy("home") - return render(request, 'agenda_culturel/event_confirm_change_status.html', {"status": status, "event": event, "cancel_url": cancel_url}) + return render( + request, + "agenda_culturel/event_confirm_change_status.html", + {"status": status, "event": event, "cancel_url": cancel_url}, + ) + def import_from_url(request): - import logging + logger = logging.getLogger(__name__) - if request.method == 'POST' and "title" in request.POST: + if request.method == "POST" and "title" in request.POST: form = EventForm(request.POST, is_authenticated=request.user.is_authenticated) if form.is_valid(): new_event = form.save() @@ -372,27 +480,35 @@ def import_from_url(request): messages.success(request, _("The event is saved.")) return HttpResponseRedirect(new_event.get_absolute_url()) else: - messages.success(request, _("The event has been submitted and will be published as soon as it has been validated by the moderation team.")) + messages.success( + request, + _( + "The event has been submitted and will be published as soon as it has been validated by the moderation team." + ), + ) return HttpResponseRedirect(reverse("home")) else: - return render(request, 'agenda_culturel/event_form.html', context={'form': form }) + return render( + request, "agenda_culturel/event_form.html", context={"form": form} + ) else: form = EventSubmissionForm() - - initial = {"start_day": date.today() + timedelta(days=1), - "start_time": "20:00", - "end_time": "22:00"} + + initial = { + "start_day": date.today() + timedelta(days=1), + "start_time": "20:00", + "end_time": "22:00", + } form_event = EventForm(initial=initial) - if request.method == 'POST': - + if request.method == "POST": form = EventSubmissionForm(request.POST) if form.is_valid(): cd = form.cleaned_data - url = cd.get('url') + url = cd.get("url") url = Extractor.clean_url(url) @@ -402,55 +518,108 @@ def import_from_url(request): event = None u2e = URL2Events(ChromiumHeadlessDownloader(), single_event=True) - events_structure = u2e.process(url, published=request.user.is_authenticated) - if events_structure is not None and "events" in events_structure and len(events_structure["events"]) > 0: - event = Event.from_structure(events_structure["events"][0], events_structure["header"]["url"]) + events_structure = u2e.process( + url, published=request.user.is_authenticated + ) + if ( + events_structure is not None + and "events" in events_structure + and len(events_structure["events"]) > 0 + ): + event = Event.from_structure( + events_structure["events"][0], + events_structure["header"]["url"], + ) # TODO: use celery to import the other events if event != None: - form = EventForm(instance=event, is_authenticated=request.user.is_authenticated) - messages.success(request, _("The event has been successfully extracted, and you can now submit it after modifying it if necessary.")) - return render(request, 'agenda_culturel/event_form.html', context={'form': form }) + form = EventForm( + instance=event, + is_authenticated=request.user.is_authenticated, + ) + messages.success( + request, + _( + "The event has been successfully extracted, and you can now submit it after modifying it if necessary." + ), + ) + return render( + request, + "agenda_culturel/event_form.html", + context={"form": form}, + ) else: - form = EventForm(initial={'reference_urls': [url]}, is_authenticated=request.user.is_authenticated) - messages.error(request, _("Unable to extract an event from the proposed URL. Please use the form below to submit the event.")) - return render(request, 'agenda_culturel/import.html', context={'form': form, 'form_event': form_event}) + form = EventForm( + initial={"reference_urls": [url]}, + is_authenticated=request.user.is_authenticated, + ) + messages.error( + request, + _( + "Unable to extract an event from the proposed URL. Please use the form below to submit the event." + ), + ) + return render( + request, + "agenda_culturel/import.html", + context={"form": form, "form_event": form_event}, + ) else: - published = [e for e in existing if e.status == Event.STATUS.PUBLISHED] + published = [ + e for e in existing if e.status == Event.STATUS.PUBLISHED + ] drafts = [e for e in existing if e.status == Event.STATUS.DRAFT] trash = [e for e in existing if e.status == Event.STATUS.TRASH] if request.user.is_authenticated or len(published) > 1: event = published[0] if len(published) > 1 else existing[0] - messages.info(request, _("This URL has already been submitted, and you can find the event below.")) + messages.info( + request, + _( + "This URL has already been submitted, and you can find the event below." + ), + ) return HttpResponseRedirect(event.get_absolute_url()) else: if len(drafts) > 0: - messages.info(request, _("This URL has already been submitted and is awaiting moderation.")) + messages.info( + request, + _( + "This URL has already been submitted and is awaiting moderation." + ), + ) elif len(trash) > 0: - messages.info(request, _("This URL has already been submitted, but has not been selected for publication by the moderation team.")) - + messages.info( + request, + _( + "This URL has already been submitted, but has not been selected for publication by the moderation team." + ), + ) - return render(request, 'agenda_culturel/import.html', context={'form': form, 'form_event': form_event}) + return render( + request, + "agenda_culturel/import.html", + context={"form": form, "form_event": form_event}, + ) class EventFilterAdmin(django_filters.FilterSet): - status = django_filters.MultipleChoiceFilter(choices=Event.STATUS.choices, widget=forms.CheckboxSelectMultiple) + status = django_filters.MultipleChoiceFilter( + choices=Event.STATUS.choices, widget=forms.CheckboxSelectMultiple + ) class Meta: model = Event - fields = ['status'] - - + fields = ["status"] class ContactMessageCreateView(SuccessMessageMixin, CreateView): model = ContactMessage template_name = "agenda_culturel/contactmessage_create_form.html" - fields = ['subject', 'name', 'email', 'message'] + fields = ["subject", "name", "email", "message"] - success_url = reverse_lazy('home') - success_message = _('Your message has been sent successfully.') + success_url = reverse_lazy("home") + success_message = _("Your message has been sent successfully.") def get_form(self, form_class=None): if form_class is None: @@ -458,41 +627,48 @@ class ContactMessageCreateView(SuccessMessageMixin, CreateView): return form_class(**self.get_form_kwargs()) - -class ContactMessageUpdateView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView): +class ContactMessageUpdateView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView +): model = ContactMessage - permission_required = ("agenda_culturel.change_contactmessage") + permission_required = "agenda_culturel.change_contactmessage" template_name = "agenda_culturel/contactmessage_moderation_form.html" - fields = ('closed', 'comments') - - success_message = _('The contact message properties has been successfully modified.') + fields = ("closed", "comments") - success_url = reverse_lazy('contactmessages') + success_message = _( + "The contact message properties has been successfully modified." + ) + success_url = reverse_lazy("contactmessages") def get_form_kwargs(self): """Return the keyword arguments for instantiating the form.""" kwargs = super().get_form_kwargs() - if hasattr(self, 'object'): - kwargs.update({'instance': self.object}) + if hasattr(self, "object"): + kwargs.update({"instance": self.object}) return kwargs - class ContactMessagesFilterAdmin(django_filters.FilterSet): - closed = django_filters.MultipleChoiceFilter(label="Status", choices=((True, _("Closed")), (False, _("Open"))), widget=forms.CheckboxSelectMultiple) + closed = django_filters.MultipleChoiceFilter( + label="Status", + choices=((True, _("Closed")), (False, _("Open"))), + widget=forms.CheckboxSelectMultiple, + ) + class Meta: model = ContactMessage - fields = ['closed'] - + fields = ["closed"] @login_required(login_url="/accounts/login/") -@permission_required('agenda_culturel.view_event') +@permission_required("agenda_culturel.view_event") def moderation(request): - filter = EventFilterAdmin(request.GET, queryset=Event.objects.all().order_by("-created_date")) + filter = EventFilterAdmin( + request.GET, queryset=Event.objects.all().order_by("-created_date") + ) paginator = Paginator(filter.qs, 10) - page = request.GET.get('page') + page = request.GET.get("page") try: response = paginator.page(page) @@ -501,14 +677,21 @@ def moderation(request): except EmptyPage: response = paginator.page(paginator.num_pages) - return render(request, 'agenda_culturel/moderation.html', {'filter': filter, 'paginator_filter': response} ) + return render( + request, + "agenda_culturel/moderation.html", + {"filter": filter, "paginator_filter": response}, + ) + @login_required(login_url="/accounts/login/") -@permission_required('agenda_culturel.view_contactmessage') +@permission_required("agenda_culturel.view_contactmessage") def contactmessages(request): - filter = ContactMessagesFilterAdmin(request.GET, queryset=ContactMessage.objects.all().order_by("-date")) + filter = ContactMessagesFilterAdmin( + request.GET, queryset=ContactMessage.objects.all().order_by("-date") + ) paginator = Paginator(filter.qs, 10) - page = request.GET.get('page') + page = request.GET.get("page") try: response = paginator.page(page) @@ -517,34 +700,47 @@ def contactmessages(request): except EmptyPage: response = paginator.page(paginator.num_pages) - return render(request, 'agenda_culturel/contactmessages.html', {'filter': filter, 'paginator_filter': response} ) - + return render( + request, + "agenda_culturel/contactmessages.html", + {"filter": filter, "paginator_filter": response}, + ) class SimpleSearchEventFilter(django_filters.FilterSet): - q = django_filters.CharFilter(method='custom_filter', label=_("Search")) + q = django_filters.CharFilter(method="custom_filter", label=_("Search")) - status = django_filters.MultipleChoiceFilter(label="Filtrer par status", + status = django_filters.MultipleChoiceFilter( + label="Filtrer par status", choices=Event.STATUS.choices, - field_name="status", - widget=forms.CheckboxSelectMultiple) + field_name="status", + widget=forms.CheckboxSelectMultiple, + ) def custom_filter(self, queryset, name, value): - search_query = SearchQuery(value, config='french') + search_query = SearchQuery(value, config="french") qs = queryset.filter( - Q(title__icontains=value) | Q(location__icontains=value) | Q(description__icontains=value)) + Q(title__icontains=value) + | Q(location__icontains=value) + | Q(description__icontains=value) + ) for f in ["title", "location", "description"]: - params = { f + "_hl": SearchHeadline(f, - search_query, - start_sel="", - stop_sel="", - config='french')} + params = { + f + + "_hl": SearchHeadline( + f, + search_query, + start_sel='', + stop_sel="", + config="french", + ) + } qs = qs.annotate(**params) return qs class Meta: model = Event - fields = ['q'] + fields = ["q"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -553,44 +749,51 @@ class SimpleSearchEventFilter(django_filters.FilterSet): class SearchEventFilter(django_filters.FilterSet): - tags = django_filters.CharFilter(lookup_expr='icontains') + tags = django_filters.CharFilter(lookup_expr="icontains") title = django_filters.CharFilter(method="hl_filter_contains") location = django_filters.CharFilter(method="hl_filter_contains") description = django_filters.CharFilter(method="hl_filter_contains") - start_day = django_filters.DateFromToRangeFilter(widget=django_filters.widgets.RangeWidget(attrs={'type': 'date'})) - status = django_filters.MultipleChoiceFilter(label="Filtrer par status", + start_day = django_filters.DateFromToRangeFilter( + widget=django_filters.widgets.RangeWidget(attrs={"type": "date"}) + ) + status = django_filters.MultipleChoiceFilter( + label="Filtrer par status", choices=Event.STATUS.choices, - field_name="status", - widget=forms.CheckboxSelectMultiple) + field_name="status", + widget=forms.CheckboxSelectMultiple, + ) o = django_filters.OrderingFilter( # tuple-mapping retains order fields=( - ('title', 'title'), - ('description', 'description'), - ('start_day', 'start_day'), + ("title", "title"), + ("description", "description"), + ("start_day", "start_day"), ), ) def hl_filter_contains(self, queryset, name, value): - # first check if it contains - filter_contains = { name + "__contains": value } + filter_contains = {name + "__contains": value} queryset = queryset.filter(**filter_contains) # then hightlight the result - search_query = SearchQuery(value, config='french') - params = { name + "_hl": SearchHeadline(name, + search_query = SearchQuery(value, config="french") + params = { + name + + "_hl": SearchHeadline( + name, search_query, - start_sel="", + start_sel='', stop_sel="", - config='french')} + config="french", + ) + } return queryset.annotate(**params) - class Meta: model = Event - fields = ['title', 'location', 'description', 'category', 'tags', 'start_day'] + fields = ["title", "location", "description", "category", "tags", "start_day"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -599,15 +802,21 @@ class SearchEventFilter(django_filters.FilterSet): def event_search(request, full=False): - if full: - filter = SearchEventFilter(request.GET, queryset=get_event_qs(request).order_by("-start_day"), request=request) + filter = SearchEventFilter( + request.GET, + queryset=get_event_qs(request).order_by("-start_day"), + request=request, + ) else: - filter = SimpleSearchEventFilter(request.GET, queryset=get_event_qs(request).order_by("-start_day"), request=request) - + filter = SimpleSearchEventFilter( + request.GET, + queryset=get_event_qs(request).order_by("-start_day"), + request=request, + ) paginator = Paginator(filter.qs, 10) - page = request.GET.get('page') + page = request.GET.get("page") try: response = paginator.page(page) @@ -616,10 +825,18 @@ def event_search(request, full=False): except EmptyPage: response = paginator.page(paginator.num_pages) - return render(request, 'agenda_culturel/search.html', {'filter': filter, - 'has_results': len(request.GET) != 0 or (len(request.GET) > 1 and "page" in request.GET), - 'paginator_filter': response, - 'full': full}) + return render( + request, + "agenda_culturel/search.html", + { + "filter": filter, + "has_results": len(request.GET) != 0 + or (len(request.GET) > 1 and "page" in request.GET), + "paginator_filter": response, + "full": full, + }, + ) + def event_search_full(request): return event_search(request, True) @@ -629,11 +846,12 @@ def event_search_full(request): ## batch importations ######################### + @login_required(login_url="/accounts/login/") -@permission_required('agenda_culturel.view_batchimportation') +@permission_required("agenda_culturel.view_batchimportation") def imports(request): paginator = Paginator(BatchImportation.objects.all().order_by("-created_date"), 10) - page = request.GET.get('page') + page = request.GET.get("page") try: response = paginator.page(page) @@ -642,33 +860,38 @@ def imports(request): except EmptyPage: response = paginator.page(paginator.num_pages) - return render(request, 'agenda_culturel/imports.html', {'paginator_filter': response} ) + return render( + request, "agenda_culturel/imports.html", {"paginator_filter": response} + ) @login_required(login_url="/accounts/login/") -@permission_required(['agenda_culturel.add_batchimportation', 'agenda_culturel.run_batchimportation']) +@permission_required( + ["agenda_culturel.add_batchimportation", "agenda_culturel.run_batchimportation"] +) def add_import(request): form = BatchImportationForm() - if request.method == 'POST': + if request.method == "POST": form = BatchImportationForm(request.POST) if form.is_valid(): - result = import_events_from_json.delay(form.data["json"]) messages.success(request, _("The import has been run successfully.")) return HttpResponseRedirect(reverse_lazy("imports")) - return render(request, 'agenda_culturel/batchimportation_form.html', {"form": form}) + return render(request, "agenda_culturel/batchimportation_form.html", {"form": form}) @login_required(login_url="/accounts/login/") -@permission_required(['agenda_culturel.view_batchimportation', 'agenda_culturel.run_batchimportation']) +@permission_required( + ["agenda_culturel.view_batchimportation", "agenda_culturel.run_batchimportation"] +) def cancel_import(request, pk): import_process = get_object_or_404(BatchImportation, pk=pk) - if request.method == 'POST': + if request.method == "POST": celery_app.control.revoke(import_process.celery_id) import_process.status = BatchImportation.STATUS.CANCELED @@ -678,17 +901,23 @@ def cancel_import(request, pk): return HttpResponseRedirect(reverse_lazy("imports")) else: cancel_url = reverse_lazy("imports") - return render(request, 'agenda_culturel/cancel_import_confirm.html', {"object": import_process, "cancel_url": cancel_url}) + return render( + request, + "agenda_culturel/cancel_import_confirm.html", + {"object": import_process, "cancel_url": cancel_url}, + ) + ######################### ## recurrent importations ######################### + @login_required(login_url="/accounts/login/") -@permission_required('agenda_culturel.view_recurrentimport') +@permission_required("agenda_culturel.view_recurrentimport") def recurrent_imports(request): paginator = Paginator(RecurrentImport.objects.all().order_by("-pk"), 10) - page = request.GET.get('page') + page = request.GET.get("page") try: response = paginator.page(page) @@ -697,36 +926,49 @@ def recurrent_imports(request): except EmptyPage: response = paginator.page(paginator.num_pages) - return render(request, 'agenda_culturel/rimports.html', {'paginator_filter': response} ) + return render( + request, "agenda_culturel/rimports.html", {"paginator_filter": response} + ) -class RecurrentImportCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): +class RecurrentImportCreateView( + LoginRequiredMixin, PermissionRequiredMixin, CreateView +): model = RecurrentImport - permission_required = ("agenda_culturel.add_recurrentimport") - success_url = reverse_lazy('recurrent_imports') + permission_required = "agenda_culturel.add_recurrentimport" + success_url = reverse_lazy("recurrent_imports") form_class = RecurrentImportForm -class RecurrentImportUpdateView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView): +class RecurrentImportUpdateView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView +): model = RecurrentImport - permission_required = ("agenda_culturel.change_recurrentimport") + permission_required = "agenda_culturel.change_recurrentimport" form_class = RecurrentImportForm - success_message = _('The recurrent import has been successfully modified.') + success_message = _("The recurrent import has been successfully modified.") -class RecurrentImportDeleteView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, DeleteView): +class RecurrentImportDeleteView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, DeleteView +): model = RecurrentImport - permission_required = ("agenda_culturel.delete_recurrentimport") - success_url = reverse_lazy('recurrent_imports') - success_message = _('The recurrent import has been successfully deleted.') + permission_required = "agenda_culturel.delete_recurrentimport" + success_url = reverse_lazy("recurrent_imports") + success_message = _("The recurrent import has been successfully deleted.") @login_required(login_url="/accounts/login/") -@permission_required(['agenda_culturel.view_recurrentimport', 'agenda_culturel.view_batchimportation']) +@permission_required( + ["agenda_culturel.view_recurrentimport", "agenda_culturel.view_batchimportation"] +) def view_rimport(request, pk): obj = get_object_or_404(RecurrentImport, pk=pk) - paginator = Paginator(BatchImportation.objects.filter(recurrentImport=pk).order_by("-created_date"), 10) - page = request.GET.get('page') + paginator = Paginator( + BatchImportation.objects.filter(recurrentImport=pk).order_by("-created_date"), + 10, + ) + page = request.GET.get("page") try: response = paginator.page(page) @@ -735,38 +977,45 @@ def view_rimport(request, pk): except EmptyPage: response = paginator.page(paginator.num_pages) - return render(request, 'agenda_culturel/page-rimport.html', {'paginator_filter': response, 'object': obj} ) - + return render( + request, + "agenda_culturel/page-rimport.html", + {"paginator_filter": response, "object": obj}, + ) @login_required(login_url="/accounts/login/") -@permission_required(['agenda_culturel.view_recurrentimport', 'agenda_culturel.run_recurrentimport']) +@permission_required( + ["agenda_culturel.view_recurrentimport", "agenda_culturel.run_recurrentimport"] +) def run_rimport(request, pk): rimport = get_object_or_404(RecurrentImport, pk=pk) - if request.method == 'POST': - + if request.method == "POST": # run recurrent import result = run_recurrent_import.delay(pk) messages.success(request, _("The import has been launched.")) return HttpResponseRedirect(reverse_lazy("view_rimport", args=[pk])) else: - return render(request, 'agenda_culturel/run_rimport_confirm.html', {"object": rimport }) + return render( + request, "agenda_culturel/run_rimport_confirm.html", {"object": rimport} + ) @login_required(login_url="/accounts/login/") -@permission_required(['agenda_culturel.view_recurrentimport', 'agenda_culturel.run_recurrentimport']) +@permission_required( + ["agenda_culturel.view_recurrentimport", "agenda_culturel.run_recurrentimport"] +) def run_all_rimports(request): - if request.method == 'POST': - + if request.method == "POST": # run recurrent import result = run_all_recurrent_imports.delay() messages.success(request, _("Imports has been launched.")) return HttpResponseRedirect(reverse_lazy("recurrent_imports")) else: - return render(request, 'agenda_culturel/run_all_rimports_confirm.html') + return render(request, "agenda_culturel/run_all_rimports_confirm.html") ######################### @@ -780,12 +1029,14 @@ class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView): @login_required(login_url="/accounts/login/") -@permission_required(['agenda_culturel.change_event', 'agenda_culturel.change_duplicatedevents']) +@permission_required( + ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] +) def merge_duplicate(request, pk): edup = get_object_or_404(DuplicatedEvents, pk=pk) form = MergeDuplicates(duplicates=edup) - if request.method == 'POST': + if request.method == "POST": form = MergeDuplicates(request.POST, duplicates=edup) if form.is_valid(): events = edup.get_duplicated() @@ -800,7 +1051,11 @@ def merge_duplicate(request, pk): if selected is None: new_event_data[f["key"]] = None elif isinstance(selected, list): - values = [x for x in [getattr(events[s], f["key"]) for s in selected] if x is not None] + values = [ + x + for x in [getattr(events[s], f["key"]) for s in selected] + if x is not None + ] if len(values) == 0: new_event_data[f["key"]] = None else: @@ -812,7 +1067,14 @@ def merge_duplicate(request, pk): new_event_data[f["key"]] = getattr(events[selected], f["key"]) for specific_tag in ["uuids", "import_sources"]: - new_event_data[specific_tag] = sum([x for x in [getattr(e, specific_tag) for e in events] if x is not None], []) + new_event_data[specific_tag] = sum( + [ + x + for x in [getattr(e, specific_tag) for e in events] + if x is not None + ], + [], + ) # create a new event that merge the selected events new_event = Event(**new_event_data) @@ -827,20 +1089,24 @@ def merge_duplicate(request, pk): messages.info(request, _("The merge has been successfully completed.")) return HttpResponseRedirect(new_event.get_absolute_url()) - return render(request, 'agenda_culturel/merge_duplicate.html', context={'form': form, 'object': edup}) + return render( + request, + "agenda_culturel/merge_duplicate.html", + context={"form": form, "object": edup}, + ) @login_required(login_url="/accounts/login/") -@permission_required(['agenda_culturel.change_event', 'agenda_culturel.change_duplicatedevents']) +@permission_required( + ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] +) def fix_duplicate(request, pk): - edup = get_object_or_404(DuplicatedEvents, pk=pk) form = FixDuplicates(nb_events=edup.nb_duplicated()) - if request.method == 'POST': + if request.method == "POST": form = FixDuplicates(request.POST, nb_events=edup.nb_duplicated()) - if form.is_valid(): if form.is_action_no_duplicates(): events = edup.get_duplicated() @@ -859,7 +1125,9 @@ def fix_duplicate(request, pk): if date is None: return HttpResponseRedirect(reverse_lazy("home")) else: - return HttpResponseRedirect(reverse_lazy("day_view", args=[date.year, date.month, date.day])) + return HttpResponseRedirect( + reverse_lazy("day_view", args=[date.year, date.month, date.day]) + ) elif form.is_action_select(): selected = form.get_selected_event(edup) @@ -871,24 +1139,44 @@ def fix_duplicate(request, pk): url = selected.get_absolute_url() edup.delete() if nb == 1: - messages.success(request, _("The selected event has been retained, while the other has been moved to the recycle bin.")) + messages.success( + request, + _( + "The selected event has been retained, while the other has been moved to the recycle bin." + ), + ) else: - messages.success(request, _("The selected event has been retained, while the others have been moved to the recycle bin.")) + messages.success( + request, + _( + "The selected event has been retained, while the others have been moved to the recycle bin." + ), + ) return HttpResponseRedirect(url) elif form.is_action_remove(): event = form.get_selected_event(edup) event.possibly_duplicated = None event.save() - messages.success(request, _("The event has been withdrawn from the group and made independent.")) + messages.success( + request, + _( + "The event has been withdrawn from the group and made independent." + ), + ) if edup.nb_duplicated() == 1: return HttpResponseRedirect(event.get_absolute_url()) else: form = FixDuplicates(nb_events=edup.nb_duplicated()) else: - return HttpResponseRedirect(reverse_lazy("merge_duplicate", args=[edup.pk])) - - return render(request, 'agenda_culturel/fix_duplicate.html', context={'form': form, 'object': edup}) + return HttpResponseRedirect( + reverse_lazy("merge_duplicate", args=[edup.pk]) + ) + return render( + request, + "agenda_culturel/fix_duplicate.html", + context={"form": form, "object": edup}, + ) class DuplicatedEventsUpdateView(LoginRequiredMixin, UpdateView): @@ -898,15 +1186,20 @@ class DuplicatedEventsUpdateView(LoginRequiredMixin, UpdateView): @login_required(login_url="/accounts/login/") -@permission_required('agenda_culturel.view_duplicatedevents') +@permission_required("agenda_culturel.view_duplicatedevents") def duplicates(request): nb_removed = DuplicatedEvents.remove_singletons() nb_similar = DuplicatedEvents.remove_similar_entries() if nb_removed > 0 or nb_similar > 0: - messages.success(request, _("Cleaning up duplicates: {} item(s) removed.").format(nb_removed + nb_similar)) + messages.success( + request, + _("Cleaning up duplicates: {} item(s) removed.").format( + nb_removed + nb_similar + ), + ) paginator = Paginator(DuplicatedEvents.objects.all(), 10) - page = request.GET.get('page') + page = request.GET.get("page") try: response = paginator.page(page) @@ -915,16 +1208,29 @@ def duplicates(request): except EmptyPage: response = paginator.page(paginator.num_pages) - return render(request, 'agenda_culturel/duplicates.html', {'filter': filter, 'paginator_filter': response} ) + return render( + request, + "agenda_culturel/duplicates.html", + {"filter": filter, "paginator_filter": response}, + ) + def set_duplicate(request, year, month, day, pk): event = get_object_or_404(Event, pk=pk) cday = CalendarDay(date(year, month, day)) - others = [e for e in cday.get_events() if e != event and (event.possibly_duplicated is None or event.possibly_duplicated != e.possibly_duplicated)] + others = [ + e + for e in cday.get_events() + if e != event + and ( + event.possibly_duplicated is None + or event.possibly_duplicated != e.possibly_duplicated + ) + ] form = SelectEventInList(events=others) - if request.method == 'POST': + if request.method == "POST": form = SelectEventInList(request.POST, events=others) if form.is_valid(): selected = [o for o in others if o.pk == int(form.cleaned_data["event"])] @@ -932,25 +1238,35 @@ def set_duplicate(request, year, month, day, pk): event.save() if request.user.is_authenticated: messages.success(request, _("The event was successfully duplicated.")) - return HttpResponseRedirect(reverse_lazy("view_duplicate", args=[event.possibly_duplicated.pk])) + return HttpResponseRedirect( + reverse_lazy("view_duplicate", args=[event.possibly_duplicated.pk]) + ) else: - messages.info(request, _("The event has been successfully flagged as a duplicate. The moderation team will deal with your suggestion shortly.")) + messages.info( + request, + _( + "The event has been successfully flagged as a duplicate. The moderation team will deal with your suggestion shortly." + ), + ) return HttpResponseRedirect(event.get_absolute_url()) - - return render(request, 'agenda_culturel/set_duplicate.html', context={'form': form, 'event': event}) - + return render( + request, + "agenda_culturel/set_duplicate.html", + context={"form": form, "event": event}, + ) ######################### ## categorisation rules ######################### + @login_required(login_url="/accounts/login/") -@permission_required('agenda_culturel.view_categorisationrule') +@permission_required("agenda_culturel.view_categorisationrule") def categorisation_rules(request): paginator = Paginator(CategorisationRule.objects.all().order_by("weight", "pk"), 10) - page = request.GET.get('page') + page = request.GET.get("page") try: response = paginator.page(page) @@ -959,34 +1275,45 @@ def categorisation_rules(request): except EmptyPage: response = paginator.page(paginator.num_pages) - return render(request, 'agenda_culturel/categorisation_rules.html', {'paginator_filter': response} ) + return render( + request, + "agenda_culturel/categorisation_rules.html", + {"paginator_filter": response}, + ) -class CategorisationRuleCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): + +class CategorisationRuleCreateView( + LoginRequiredMixin, PermissionRequiredMixin, CreateView +): model = CategorisationRule - permission_required = ("agenda_culturel.add_categorisationrule") - success_url = reverse_lazy('categorisation_rules') + permission_required = "agenda_culturel.add_categorisationrule" + success_url = reverse_lazy("categorisation_rules") form_class = CategorisationRuleImportForm -class CategorisationRuleUpdateView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView): +class CategorisationRuleUpdateView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView +): model = CategorisationRule - permission_required = ("agenda_culturel.change_categorisationrule") + permission_required = "agenda_culturel.change_categorisationrule" form_class = CategorisationRuleImportForm - success_url = reverse_lazy('categorisation_rules') - success_message = _('The categorisation rule has been successfully modified.') + success_url = reverse_lazy("categorisation_rules") + success_message = _("The categorisation rule has been successfully modified.") -class CategorisationRuleDeleteView(SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, DeleteView): +class CategorisationRuleDeleteView( + SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, DeleteView +): model = CategorisationRule - permission_required = ("agenda_culturel.delete_categorisationrule") - success_url = reverse_lazy('categorisation_rules') - success_message = _('The categorisation rule has been successfully deleted.') + permission_required = "agenda_culturel.delete_categorisationrule" + success_url = reverse_lazy("categorisation_rules") + success_message = _("The categorisation rule has been successfully deleted.") + @login_required(login_url="/accounts/login/") -@permission_required('agenda_culturel.apply_categorisationrules') +@permission_required("agenda_culturel.apply_categorisationrules") def apply_categorisation_rules(request): - - if request.method == 'POST': + if request.method == "POST": form = CategorisationForm(request.POST) if form.is_valid(): nb = 0 @@ -1000,15 +1327,34 @@ def apply_categorisation_rules(request): if nb != 0: if nb == 1: - messages.success(request, _("The rules were successfully applied and 1 event was categorised.")) + messages.success( + request, + _( + "The rules were successfully applied and 1 event was categorised." + ), + ) else: - messages.success(request, _("The rules were successfully applied and {} events were categorised.").format(nb)) + messages.success( + request, + _( + "The rules were successfully applied and {} events were categorised." + ).format(nb), + ) else: - messages.info(request, _("The rules were successfully applied and no events were categorised.")) + messages.info( + request, + _( + "The rules were successfully applied and no events were categorised." + ), + ) return HttpResponseRedirect(reverse_lazy("categorisation_rules")) else: - return render(request, 'agenda_culturel/categorise_events_form.html', context={'form': form}) + return render( + request, + "agenda_culturel/categorise_events_form.html", + context={"form": form}, + ) else: # first we check if events are not correctly categorised to_categorise = [] @@ -1017,7 +1363,6 @@ def apply_categorisation_rules(request): if c and c != e.category: to_categorise.append((e, c)) - # then we apply rules on events without category nb = 0 for e in Event.objects.filter(category=Category.get_default_category_id()): @@ -1029,20 +1374,41 @@ def apply_categorisation_rules(request): # set messages if nb != 0: if nb == 1: - messages.success(request, _("The rules were successfully applied and 1 event was categorised.")) + messages.success( + request, + _( + "The rules were successfully applied and 1 event was categorised." + ), + ) else: - messages.success(request, _("The rules were successfully applied and {} events were categorised.").format(nb)) + messages.success( + request, + _( + "The rules were successfully applied and {} events were categorised." + ).format(nb), + ) else: - messages.info(request, _("The rules were successfully applied and no events were categorised.")) + messages.info( + request, + _( + "The rules were successfully applied and no events were categorised." + ), + ) if len(to_categorise) != 0: form = CategorisationForm(events=to_categorise) - return render(request, 'agenda_culturel/categorise_events_form.html', context={'form': form, - 'events': dict((e.pk, e) for e, c in to_categorise), - 'categories': dict((e.pk, c) for e, c in to_categorise)}) + return render( + request, + "agenda_culturel/categorise_events_form.html", + context={ + "form": form, + "events": dict((e.pk, e) for e, c in to_categorise), + "categories": dict((e.pk, c) for e, c in to_categorise), + }, + ) else: return HttpResponseRedirect(reverse_lazy("categorisation_rules")) - + ######################### ## Moderation Q&A @@ -1052,78 +1418,94 @@ def apply_categorisation_rules(request): class ModerationQuestionListView(PermissionRequiredMixin, ListView): model = ModerationQuestion paginate_by = 10 - permission_required = ("agenda_culturel.view_moderationquestion") + permission_required = "agenda_culturel.view_moderationquestion" -class ModerationQuestionCreateView(SuccessMessageMixin, PermissionRequiredMixin, CreateView): + +class ModerationQuestionCreateView( + SuccessMessageMixin, PermissionRequiredMixin, CreateView +): model = ModerationQuestion - permission_required = ("agenda_culturel.add_moderationquestion") - + permission_required = "agenda_culturel.add_moderationquestion" + def get_success_url(self): - return reverse_lazy('view_mquestion', kwargs={'pk': self.object.pk}) + return reverse_lazy("view_mquestion", kwargs={"pk": self.object.pk}) + form_class = ModerationQuestionForm - success_message = _('The moderation question has been created with success.') + success_message = _("The moderation question has been created with success.") class ModerationQuestionDetailView(PermissionRequiredMixin, DetailView): model = ModerationQuestion - permission_required = ("agenda_culturel.view_moderationquestion", "agenda_culturel.view_moderationanswer") + permission_required = ( + "agenda_culturel.view_moderationquestion", + "agenda_culturel.view_moderationanswer", + ) class ModerationQuestionUpdateView(PermissionRequiredMixin, UpdateView): model = ModerationQuestion - fields = ['question'] - permission_required = ("agenda_culturel.change_moderationquestion") + fields = ["question"] + permission_required = "agenda_culturel.change_moderationquestion" + class ModerationQuestionDeleteView(PermissionRequiredMixin, DeleteView): model = ModerationQuestion - permission_required = ("agenda_culturel.delete_moderationquestion") - success_url = reverse_lazy('view_mquestions') + permission_required = "agenda_culturel.delete_moderationquestion" + success_url = reverse_lazy("view_mquestions") -class ModerationAnswerCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): +class ModerationAnswerCreateView( + PermissionRequiredMixin, SuccessMessageMixin, CreateView +): model = ModerationAnswer - permission_required = ("agenda_culturel.add_answerquestion") + permission_required = "agenda_culturel.add_answerquestion" form_class = ModerationAnswerForm - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['question'] = get_object_or_404(ModerationQuestion, pk=self.kwargs['qpk']) + context["question"] = get_object_or_404( + ModerationQuestion, pk=self.kwargs["qpk"] + ) return context def form_valid(self, form): - form.instance.question = ModerationQuestion.objects.get(pk=self.kwargs['qpk']) + form.instance.question = ModerationQuestion.objects.get(pk=self.kwargs["qpk"]) return super().form_valid(form) def get_success_url(self): - return reverse_lazy('view_mquestion', kwargs={'pk': self.kwargs['qpk']}) + return reverse_lazy("view_mquestion", kwargs={"pk": self.kwargs["qpk"]}) + - class ModerationAnswerUpdateView(PermissionRequiredMixin, UpdateView): model = ModerationAnswer - fields = ['answer', 'adds_tags', 'removes_tags'] - permission_required = ("agenda_culturel.change_answerquestion") + fields = ["answer", "adds_tags", "removes_tags"] + permission_required = "agenda_culturel.change_answerquestion" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['question'] = get_object_or_404(ModerationQuestion, pk=self.kwargs['qpk']) + context["question"] = get_object_or_404( + ModerationQuestion, pk=self.kwargs["qpk"] + ) return context def get_success_url(self): - return reverse_lazy('view_mquestion', kwargs={'pk': self.kwargs['qpk']}) + return reverse_lazy("view_mquestion", kwargs={"pk": self.kwargs["qpk"]}) + class ModerationAnswerDeleteView(PermissionRequiredMixin, DeleteView): model = ModerationAnswer - permission_required = ("agenda_culturel.delete_answerquestion") + permission_required = "agenda_culturel.delete_answerquestion" ######################### ## Places ######################### + class PlaceListView(ListView): model = Place paginate_by = 10 - ordering = ['name__unaccent'] + ordering = ["name__unaccent"] class PlaceDetailView(ListView): @@ -1132,12 +1514,12 @@ class PlaceDetailView(ListView): paginate_by = 10 def get_queryset(self): - self.place = get_object_or_404(Place, pk=self.kwargs['pk']) + self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) return Event.objects.filter(exact_location=self.place).order_by("-start_day") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['object'] = self.place + context["object"] = self.place return context @@ -1148,13 +1530,15 @@ class UpdatePlaces: if not hasattr(self, "nb_applied"): self.nb_applied = 0 - + # if required, find all matching events if form.apply(): self.nb_applied += p.associate_matching_events() if self.nb_applied > 1: - messages.success(self.request, _("{} events have been updated.").format(self.nb_applied)) + messages.success( + self.request, _("{} events have been updated.").format(self.nb_applied) + ) elif self.nb_applied == 1: messages.success(self.request, _("1 event has been updated.")) else: @@ -1162,34 +1546,39 @@ class UpdatePlaces: return result - -class PlaceUpdateView(UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, UpdateView): +class PlaceUpdateView( + UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): model = Place - permission_required = ("agenda_culturel.change_place") - success_message = _('The place has been successfully updated.') + permission_required = "agenda_culturel.change_place" + success_message = _("The place has been successfully updated.") form_class = PlaceForm -class PlaceCreateView(UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, CreateView): +class PlaceCreateView( + UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): model = Place - permission_required = ("agenda_culturel.add_place") - success_message = _('The place has been successfully created.') + permission_required = "agenda_culturel.add_place" + success_message = _("The place has been successfully created.") form_class = PlaceForm - + class PlaceDeleteView(PermissionRequiredMixin, DeleteView): model = Place - permission_required = ("agenda_culturel.delete_place") - success_url = reverse_lazy('view_places') + permission_required = "agenda_culturel.delete_place" + success_url = reverse_lazy("view_places") + class UnknownPlacesListView(PermissionRequiredMixin, ListView): model = Event - permission_required = ("agenda_culturel.add_place") + permission_required = "agenda_culturel.add_place" paginate_by = 10 - ordering = ['-pk'] - template_name = 'agenda_culturel/place_unknown_list.html' + ordering = ["-pk"] + template_name = "agenda_culturel/place_unknown_list.html" queryset = Event.objects.filter(exact_location__isnull=True) + def fix_unknown_places(request): # get all places places = Place.objects.all() @@ -1206,7 +1595,7 @@ def fix_unknown_places(request): continue # update events with a location Event.objects.bulk_update(to_be_updated, fields=["exact_location"]) - + # create a success message nb = len(to_be_updated) if nb > 1: @@ -1218,29 +1607,38 @@ def fix_unknown_places(request): # come back to the list of places return HttpResponseRedirect(reverse_lazy("view_unknown_places")) - + class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): model = Event - permission_required = ("agenda_culturel.change_place", "agenda_culturel.change_event") + permission_required = ( + "agenda_culturel.change_place", + "agenda_culturel.change_event", + ) form_class = EventAddPlaceForm - template_name = 'agenda_culturel/place_unknown_form.html' + template_name = "agenda_culturel/place_unknown_form.html" def form_valid(self, form): - - self.modified_event = form.cleaned_data.get('place') + self.modified_event = form.cleaned_data.get("place") self.add_alias = form.cleaned_data.get("add_alias") result = super().form_valid(form) - if form.cleaned_data.get('place'): - messages.success(self.request, _("The selected place has been assigned to the event.")) + if form.cleaned_data.get("place"): + messages.success( + self.request, _("The selected place has been assigned to the event.") + ) if form.cleaned_data.get("add_alias"): - messages.success(self.request, _("A new alias has been added to the selected place.")) + messages.success( + self.request, _("A new alias has been added to the selected place.") + ) - nb_applied = form.cleaned_data.get('place').associate_matching_events() + nb_applied = form.cleaned_data.get("place").associate_matching_events() if nb_applied > 1: - messages.success(self.request, _("{} events have been updated.").format(nb_applied)) + messages.success( + self.request, + _("{} events have been updated.").format(nb_applied), + ) elif nb_applied == 1: messages.success(self.request, _("1 event has been updated.")) else: @@ -1250,14 +1648,13 @@ class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateVi def get_success_url(self): if self.modified_event: - return reverse_lazy('view_unknown_places') + return reverse_lazy("view_unknown_places") else: param = "?add=1" if self.add_alias else "" - return reverse_lazy('add_place_from_event', args=[self.object.pk]) + param + return reverse_lazy("add_place_from_event", args=[self.object.pk]) + param class PlaceFromEventCreateView(PlaceCreateView): - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["event"] = self.event @@ -1265,9 +1662,9 @@ class PlaceFromEventCreateView(PlaceCreateView): def get_initial(self, *args, **kwargs): initial = super().get_initial(**kwargs) - self.event = get_object_or_404(Event, pk=self.kwargs['pk']) + self.event = get_object_or_404(Event, pk=self.kwargs["pk"]) if self.event.location and "add" in self.request.GET: - initial['aliases'] = [self.event.location] + initial["aliases"] = [self.event.location] return initial def form_valid(self, form):