formatage black sur les src

This commit is contained in:
SebF 2024-05-02 11:25:54 +02:00
parent 9652024852
commit 3daadf2b86
29 changed files with 2627 additions and 1034 deletions

View File

@ -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},
}
DynamicArrayField: {"urls": URLWidget},
}

View File

@ -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
return self.calendar_days_list()[0].events

View File

@ -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"

View File

@ -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

View File

@ -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 = '<div class="grid">'
for i, e in enumerate(self.duplicates.get_duplicated()):
result += '<div class="grid entete-badge">'
result += '<div class="badge-large">' + int_to_abc(i) + '</div>'
result += '<ul>'
result += '<li><a href="' + e.get_absolute_url() + '">' + e.title + '</a></li>'
result += '<li>Création&nbsp;: ' + localize(localtime(e.created_date)) + '</li>'
result += '<li>Dernière modification&nbsp;: ' + localize(localtime(e.modified_date)) + '</li>'
result += '<div class="badge-large">' + int_to_abc(i) + "</div>"
result += "<ul>"
result += (
'<li><a href="' + e.get_absolute_url() + '">' + e.title + "</a></li>"
)
result += (
"<li>Création&nbsp;: " + localize(localtime(e.created_date)) + "</li>"
)
result += (
"<li>Dernière modification&nbsp;: "
+ localize(localtime(e.modified_date))
+ "</li>"
)
if e.imported_date:
result += '<li>Dernière importation&nbsp;: ' + localize(localtime(e.imported_date)) + '</li>'
result += '</ul>'
result += '</div>'
result += '</div>'
result += (
"<li>Dernière importation&nbsp;: "
+ localize(localtime(e.imported_date))
+ "</li>"
)
result += "</ul>"
result += "</div>"
result += "</div>"
for e in self.duplicates.get_items_comparison():
key = e["key"]
result += "<h3>" + event_field_verbose_name(e["key"]) + "</h3>"
if e["similar"]:
result += '<div class="comparison-item">Identique&nbsp;:' + str(field_to_html(e["values"], e["key"])) + '</div>'
result += (
'<div class="comparison-item">Identique&nbsp;:'
+ str(field_to_html(e["values"], e["key"]))
+ "</div>"
)
else:
result += '<fieldset>'
result += "<fieldset>"
result += '<div class="grid comparison-item">'
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 += '<div class="duplicated">'
id = 'id_' + key + '_' + str(i)
value = 'event' + auc[i]
id = "id_" + key + "_" + str(i)
value = "event" + auc[i]
result += '<input id="' + id + '" name="' + key + '"'
if key in MergeDuplicates.checkboxes_fields:
@ -219,13 +313,18 @@ class MergeDuplicates(Form):
if checked == value:
result += " checked"
result += ' value="' + value + '"'
result += '>'
result += '<div class="badge-small">' + int_to_abc(i) + '</div>' + str(field_to_html(v, e["key"])) + '</div>'
result += ">"
result += (
'<div class="badge-small">'
+ int_to_abc(i)
+ "</div>"
+ str(field_to_html(v, e["key"]))
+ "</div>"
)
result += "</div></fieldset>"
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('<div class="grid"><div>' + super().as_p() + '</div><div><div class="map-widget">' +
'<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div><p>Cliquez pour ajuster la position GPS</p></div></div></div>')
fields = "__all__"
widgets = {"location": TextInput()}
def as_grid(self):
return mark_safe(
'<div class="grid"><div>'
+ super().as_p()
+ '</div><div><div class="map-widget">'
+ '<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div><p>Cliquez pour ajuster la position GPS</p></div></div></div>'
)
def apply(self):
return self.cleaned_data.get("apply_to_all")
return self.cleaned_data.get("apply_to_all")

View File

@ -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')]
__all__ = [
basename(f)[:-3] for f in modules if isfile(f) and not f.endswith("__init__.py")
]

View File

@ -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,
)

View File

@ -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 = (.*)</script>", str(script), re.S)
search = re.search(
r"window.fullCalendarContent = (.*)</script>", 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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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)

View File

@ -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)]
return [ICALExtractor(), FacebookEventExtractor(single_event=False)]

View File

@ -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
return None

View File

@ -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)
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,
)

View File

@ -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()

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",
}

View File

@ -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"

View File

@ -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 = '<style type="text/css">'
cats = [{"color": c.color, "css_class": c.css_class()} for c in Category.objects.all()]
cats.append({"color": Category.default_color, "css_class": Category.default_css_class})
cats = [
{"color": c.color, "css_class": c.css_class()} for c in Category.objects.all()
]
cats.append(
{"color": Category.default_color, "css_class": Category.default_css_class}
)
for c in cats:
result += "." + c["css_class"] + " {"
result += background_color_adjust_color(adjust_lightness_saturation(c["color"], .2, 0.8), 0.8)
result += background_color_adjust_color(
adjust_lightness_saturation(c["color"], 0.2, 0.8), 0.8
)
result += "}"
result += "*:hover>." + c["css_class"] + " {"
result += background_color_adjust_color(adjust_lightness_saturation(c["color"], 0.02, 1.0))
result += background_color_adjust_color(
adjust_lightness_saturation(c["color"], 0.02, 1.0)
)
result += "}"
result += "." + c["css_class"] + ".circ-cat, "
result += "form ." + c["css_class"] + ", "
result += ".selected ." + c["css_class"] + " {"
@ -91,22 +105,27 @@ def css_categories():
result += "." + c["css_class"] + ".circ-cat:hover, "
result += "form ." + c["css_class"] + ":hover, "
result += "a.selected:hover ." + c["css_class"] + " {"
result += background_color_adjust_color(adjust_lightness_saturation(c["color"], 0.2, 1.2))
result += background_color_adjust_color(
adjust_lightness_saturation(c["color"], 0.2, 1.2)
)
result += "}"
result += "." + c["css_class"] + ".circ-cat.recurrent:hover, "
result += ".selected.recurrent:hover ." + c["css_class"] + " {"
result += "background: none;"
result += "color: " + adjust_color(adjust_lightness_saturation(c["color"], 0.2, 1.2)) + ";"
result += (
"color: "
+ adjust_color(adjust_lightness_saturation(c["color"], 0.2, 1.2))
+ ";"
)
result += "}"
result += '</style>'
result += "</style>"
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 = ('<span class="' + css_class + '">' + picto_from_name("repeat", name + ' [récurrent]') + '</span>') if recurrence else ('<span class="cat ' + css_class + '"></span>')
content = (
(
'<span class="'
+ css_class
+ '">'
+ picto_from_name("repeat", name + " [récurrent]")
+ "</span>"
)
if recurrence
else ('<span class="cat ' + css_class + '"></span>')
)
if url is None:
return mark_safe('<span class="small-cat' + class_recurrence + class_contrast + class_selected + '" role="button">' + content + " " + name + "</span>")
return mark_safe(
'<span class="small-cat'
+ class_recurrence
+ class_contrast
+ class_selected
+ '" role="button">'
+ content
+ " "
+ name
+ "</span>"
)
else:
return mark_safe('<a class="small-cat' + class_recurrence + class_contrast + class_selected + '" role="button" href="' + url + '">' + content + " " + name + "</a>")
return mark_safe(
'<a class="small-cat'
+ class_recurrence
+ class_contrast
+ class_selected
+ '" role="button" href="'
+ url
+ '">'
+ content
+ " "
+ name
+ "</a>"
)
@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('<span class="cat recurrent ' + c + ' circ-cat">' + picto_from_name("repeat", n + ' [récurrent]') + "</span>")
return mark_safe(
'<span class="cat recurrent '
+ c
+ ' circ-cat">'
+ picto_from_name("repeat", n + " [récurrent]")
+ "</span>"
)
else:
return mark_safe('<span class="cat ' + c + ' circ-cat" data-tooltip="' + n + '"></span>')
return mark_safe(
'<span class="cat ' + c + ' circ-cat" data-tooltip="' + n + '"></span>'
)
@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]))
return mark_safe(
" ".join(
[small_cat(c, current_url + "?category=" + str(c.pk)) for c in cats]
)
)

View File

@ -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('<a href="' + reverse_lazy("contactmessages") + '?closed=false" class="badge" data-placement="' + placement + '"data-tooltip="' + str(nb_open) + ' message' + pluralize(nb_open) + ' à traiter">' + picto_from_name("mail") + " " + str(nb_open) + '</a>')
return mark_safe(
'<a href="'
+ reverse_lazy("contactmessages")
+ '?closed=false" class="badge" data-placement="'
+ placement
+ '"data-tooltip="'
+ str(nb_open)
+ " message"
+ pluralize(nb_open)
+ ' à traiter">'
+ picto_from_name("mail")
+ " "
+ str(nb_open)
+ "</a>"
)
else:
return ""
return ""

View File

@ -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('<a href="' + reverse_lazy("duplicates") + '" class="badge" data-placement="' + placement + '" data-tooltip="' + str(nb_duplicated) + ' dupliqué' + pluralize(nb_duplicated) + ' à valider">' + picto_from_name("copy") + " " + str(nb_duplicated) + '</a>')
return mark_safe(
'<a href="'
+ reverse_lazy("duplicates")
+ '" class="badge" data-placement="'
+ placement
+ '" data-tooltip="'
+ str(nb_duplicated)
+ " dupliqué"
+ pluralize(nb_duplicated)
+ ' à valider">'
+ picto_from_name("copy")
+ " "
+ str(nb_duplicated)
+ "</a>"
)
else:
return ""
return ""

View File

@ -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('<a href="' + reverse_lazy("moderation") + '?status=draft" class="badge" data-placement="' + placement + '" data-tooltip="' + str(nb_drafts) + ' brouillon' + pluralize(nb_drafts) + ' à valider">' + picto_from_name("calendar") + " " + str(nb_drafts) + '</a>')
return mark_safe(
'<a href="'
+ reverse_lazy("moderation")
+ '?status=draft" class="badge" data-placement="'
+ placement
+ '" data-tooltip="'
+ str(nb_drafts)
+ " brouillon"
+ pluralize(nb_drafts)
+ ' à valider">'
+ picto_from_name("calendar")
+ " "
+ str(nb_drafts)
+ "</a>"
)
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('<a href="' + reverse_lazy("view_unknown_places") + '" class="badge" data-placement="' + placement + '" data-tooltip="' + str(nb_unknown) + ' événement' + pluralize(nb_unknown) + ' sans lieu défini">' + picto_from_name("map-pin") + " " + str(nb_unknown) + '</a>')
return mark_safe(
'<a href="'
+ reverse_lazy("view_unknown_places")
+ '" class="badge" data-placement="'
+ placement
+ '" data-tooltip="'
+ str(nb_unknown)
+ " événement"
+ pluralize(nb_unknown)
+ ' sans lieu défini">'
+ picto_from_name("map-pin")
+ " "
+ str(nb_unknown)
+ "</a>"
)
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("<ul>" + "".join(['<li><a href="' + u + '">' + u + '</a></li>' for u in field]) + "</ul>")
return mark_safe(
"<ul>"
+ "".join(['<li><a href="' + u + '">' + u + "</a></li>" for u in field])
+ "</ul>"
)
elif key == "image":
return mark_safe('<a href="' + field + '">' + field + '</a>')
return mark_safe('<a href="' + field + '">' + field + "</a>")
elif key == "local_image":
if field:
return mark_safe('<img src="' + field.url + '" />')
else:
return "-"
else:
return field
return field

View File

@ -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('<a href="' + reverse_lazy("recurrent_imports") + '" class="badge error" data-placement="' + placement + '" data-tooltip="' + str(nb_failed) + ' importation' + pluralize(nb_failed) + ' récurrente' + pluralize(nb_failed) + ' en erreur">' + picto_from_name("alert-triangle") + " " + str(nb_failed) + '</a>')
return mark_safe(
'<a href="'
+ reverse_lazy("recurrent_imports")
+ '" class="badge error" data-placement="'
+ placement
+ '" data-tooltip="'
+ str(nb_failed)
+ " importation"
+ pluralize(nb_failed)
+ " récurrente"
+ pluralize(nb_failed)
+ ' en erreur">'
+ picto_from_name("alert-triangle")
+ " "
+ str(nb_failed)
+ "</a>"
)
else:
return ""
return ""

View File

@ -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))
return "".join(map(str, args))

View File

@ -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('<a href="' + reverse_lazy('view_tag', {"tag": tag}) +'" role="button" class="small-cat' + strike_class + '">' + tag + '</a>')
return mark_safe(
'<a href="'
+ reverse_lazy("view_tag", {"tag": tag})
+ '" role="button" class="small-cat'
+ strike_class
+ '">'
+ tag
+ "</a>"
)
else:
return mark_safe('<span role="button" class="small-cat' + strike_class + '">' + tag + '</span>')
return mark_safe(
'<span role="button" class="small-cat'
+ strike_class
+ '">'
+ tag
+ "</span>"
)
@register.filter
def tag_button_strike(tag, link=False):
return tag_button(tag, link, strike=True)
return tag_button(tag, link, strike=True)

View File

@ -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 = '<svg width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + \
'<use href="' + static("images/feather-sprite.svg") + '#' + name + '" />' + \
'</svg>'
result = (
'<svg width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
+ '<use href="'
+ static("images/feather-sprite.svg")
+ "#"
+ name
+ '" />'
+ "</svg>"
)
if datatooltip != "":
result = '<span data-tooltip="' + datatooltip + '">' + result + '</span>'
result = '<span data-tooltip="' + datatooltip + '">' + result + "</span>"
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_", ""))

View File

@ -11,34 +11,58 @@ from .views import *
urlpatterns = [
path("", home, name="home"),
path("semaine/<int:year>/<int:week>/", week_view, name='week_view'),
path("mois/<int:year>/<int:month>/", month_view, name='month_view'),
path("jour/<int:year>/<int:month>/<int:day>/", day_view, name='day_view'),
path("semaine/<int:year>/<int:week>/", week_view, name="week_view"),
path("mois/<int:year>/<int:month>/", month_view, name="month_view"),
path("jour/<int:year>/<int:month>/<int:day>/", 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/<t>/", view_tag, name='view_tag'),
path("tags/", tag_list, name='view_all_tags'),
path("moderation/", moderation, name='moderation'),
path("event/<int:year>/<int:month>/<int:day>/<int:pk>-<extra>", EventDetailView.as_view(), name="view_event"),
path("tag/<t>/", view_tag, name="view_tag"),
path("tags/", tag_list, name="view_all_tags"),
path("moderation/", moderation, name="moderation"),
path(
"event/<int:year>/<int:month>/<int:day>/<int:pk>-<extra>",
EventDetailView.as_view(),
name="view_event",
),
path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
path("event/<int:pk>/change-status/<status>", change_status_event, name="change_status_event"),
path(
"event/<int:pk>/change-status/<status>",
change_status_event,
name="change_status_event",
),
path("event/<int:pk>/delete", EventDeleteView.as_view(), name="delete_event"),
path("event/<int:year>/<int:month>/<int:day>/<int:pk>/set_duplicate", set_duplicate, name="set_duplicate"),
path(
"event/<int:year>/<int:month>/<int:day>/<int:pk>/set_duplicate",
set_duplicate,
name="set_duplicate",
),
path("event/<int:pk>/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/<int:pk>/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/<int:pk>', ContactMessageUpdateView.as_view(), name='contactmessage'),
path(
"static-content/create",
StaticContentCreateView.as_view(),
name="create_static_content",
),
path(
"static-content/<int:pk>/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/<int:pk>",
ContactMessageUpdateView.as_view(),
name="contactmessage",
),
path("imports/", imports, name="imports"),
path("imports/add", add_import, name="add_import"),
path("imports/<int:pk>/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/<int:pk>/view", view_rimport, name="view_rimport"),
path("rimports/<int:pk>/edit", RecurrentImportUpdateView.as_view(), name="edit_rimport"),
path("rimports/<int:pk>/delete", RecurrentImportDeleteView.as_view(), name="delete_rimport"),
path(
"rimports/<int:pk>/edit",
RecurrentImportUpdateView.as_view(),
name="edit_rimport",
),
path(
"rimports/<int:pk>/delete",
RecurrentImportDeleteView.as_view(),
name="delete_rimport",
),
path("rimports/<int:pk>/run", run_rimport, name="run_rimport"),
path("catrules/", categorisation_rules, name="categorisation_rules"),
path("catrules/add", CategorisationRuleCreateView.as_view(), name="add_catrule"),
path("catrules/<int:pk>/edit", CategorisationRuleUpdateView.as_view(), name="edit_catrule"),
path("catrules/<int:pk>/delete", CategorisationRuleDeleteView.as_view(), name="delete_catrule"),
path(
"catrules/<int:pk>/edit",
CategorisationRuleUpdateView.as_view(),
name="edit_catrule",
),
path(
"catrules/<int:pk>/delete",
CategorisationRuleDeleteView.as_view(),
name="delete_catrule",
),
path("catrules/apply", apply_categorisation_rules, name="apply_catrules"),
path("duplicates/", duplicates, name="duplicates"),
path("duplicates/<int:pk>", DuplicatedEventsDetailView.as_view(), name="view_duplicate"),
path(
"duplicates/<int:pk>",
DuplicatedEventsDetailView.as_view(),
name="view_duplicate",
),
path("duplicates/<int:pk>/fix", fix_duplicate, name="fix_duplicate"),
path("duplicates/<int:pk>/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/<int:pk>/", ModerationQuestionDetailView.as_view(), name="view_mquestion"),
path("mquestions/<int:pk>/edit", ModerationQuestionUpdateView.as_view(), name="edit_mquestion"),
path("mquestions/<int:pk>/delete", ModerationQuestionDeleteView.as_view(), name="delete_mquestion"),
path("mquestions/<int:qpk>/answers/add", ModerationAnswerCreateView.as_view(), name="add_manswer"),
path("mquestions/<int:qpk>/answers/<int:pk>/edit", ModerationAnswerUpdateView.as_view(), name="edit_manswer"),
path("mquestions/<int:qpk>/answers/<int:pk>/delete", ModerationAnswerDeleteView.as_view(), name="delete_manswer"),
path(
"mquestions/add", ModerationQuestionCreateView.as_view(), name="add_mquestion"
),
path(
"mquestions/<int:pk>/",
ModerationQuestionDetailView.as_view(),
name="view_mquestion",
),
path(
"mquestions/<int:pk>/edit",
ModerationQuestionUpdateView.as_view(),
name="edit_mquestion",
),
path(
"mquestions/<int:pk>/delete",
ModerationQuestionDeleteView.as_view(),
name="delete_mquestion",
),
path(
"mquestions/<int:qpk>/answers/add",
ModerationAnswerCreateView.as_view(),
name="add_manswer",
),
path(
"mquestions/<int:qpk>/answers/<int:pk>/edit",
ModerationAnswerUpdateView.as_view(),
name="edit_manswer",
),
path(
"mquestions/<int:qpk>/answers/<int:pk>/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/<int:pk>", PlaceDetailView.as_view(), name="view_place"),
@ -73,11 +143,22 @@ urlpatterns = [
path("place/<int:pk>/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/<int:pk>", PlaceFromEventCreateView.as_view(), name="add_place_from_event"),
path("events/unknown-places", UnknownPlacesListView.as_view(), name="view_unknown_places"),
path(
"places/add/<int:pk>",
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/<int:pk>/addplace", UnknownPlaceAddView.as_view(), name="add_place_to_event"),
path(
"event/<int:pk>/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'
handler404 = "agenda_culturel.views.page_not_found"
handler500 = "agenda_culturel.views.internal_server_error"

File diff suppressed because it is too large Load Diff