diff --git a/README.md b/README.md
index c98d408..98ae204 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ Pour ajouter une nouvelle source custom:
### Récupérer un dump du prod sur un serveur dev
* sur le serveur de dev:
- * ```docker exec -i agenda_culturel-backend python3 manage.py dumpdata --format=json --exclude=admin.logentry --exclude=auth.group --exclude=auth.permission --exclude=auth.user --exclude=contenttypes --indent=2 > fixtures/postgres-backup-20241101.json``` (à noter qu'ici on oublie les comptes, qu'il faudra recréer)
+ * ```docker exec -i agenda_culturel-backend python3 manage.py dumpdata --natural-foreign --natural-primary --format=json --exclude=admin.logentry --indent=2 > fixtures/postgres-backup-20241101.json``` (à noter qu'ici on oublie les comptes, qu'il faudra recréer)
* sur le serveur de prod:
* On récupère le dump json ```scp $SERVEUR:$PATH/fixtures/postgres-backup-20241101.json src/fixtures/```
* ```scripts/reset-database.sh FIXTURE COMMIT``` où ```FIXTURE``` est le timestamp dans le nom de la fixture, et ```COMMIT``` est l'ID du commit git correspondant à celle en prod sur le serveur au moment de la création de la fixture
diff --git a/deployment/Dockerfile b/deployment/Dockerfile
index 533a9e8..7f038bf 100644
--- a/deployment/Dockerfile
+++ b/deployment/Dockerfile
@@ -5,10 +5,11 @@ WORKDIR /usr/src/app
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && \
- apt-get install --no-install-recommends -y build-essential libpq-dev gettext chromium-driver gdal-bin \
+ apt-get install --no-install-recommends -y build-essential libpq-dev gettext chromium-driver gdal-bin fonts-symbola \
&& rm -rf /var/lib/apt/lists/*
+
COPY src/requirements.txt ./requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
diff --git a/experimentations/get_le_rio.py b/experimentations/get_le_rio.py
new file mode 100755
index 0000000..35e0364
--- /dev/null
+++ b/experimentations/get_le_rio.py
@@ -0,0 +1,43 @@
+#!/usr/bin/python3
+# coding: utf-8
+
+import os
+import json
+import sys
+
+# getting the name of the directory
+# where the this file is present.
+current = os.path.dirname(os.path.realpath(__file__))
+
+# Getting the parent directory name
+# where the current directory is present.
+parent = os.path.dirname(current)
+
+# adding the parent directory to
+# the sys.path.
+sys.path.append(parent)
+
+from src.agenda_culturel.import_tasks.downloader import *
+from src.agenda_culturel.import_tasks.extractor import *
+from src.agenda_culturel.import_tasks.importer import *
+from src.agenda_culturel.import_tasks.custom_extractors import *
+
+
+
+
+
+if __name__ == "__main__":
+
+ u2e = URL2Events(SimpleDownloader(), lerio.CExtractor())
+ url = "https://www.cinemalerio.com/evenements/"
+ url_human = "https://www.cinemalerio.com/evenements/"
+
+ try:
+ events = u2e.process(url, url_human, cache = "cache-le-rio.html", default_values = {"location": "Cinéma le Rio", "category": "Cinéma"}, published = True)
+
+ exportfile = "events-le-roi.json"
+ print("Saving events to file {}".format(exportfile))
+ with open(exportfile, "w") as f:
+ json.dump(events, f, indent=4, default=str)
+ except Exception as e:
+ print("Exception: " + str(e))
diff --git a/scripts/reset-database.sh b/scripts/reset-database.sh
index a33b74c..23afac3 100755
--- a/scripts/reset-database.sh
+++ b/scripts/reset-database.sh
@@ -73,6 +73,10 @@ git checkout $COMMIT
echobold "Setup database stucture according to the selected commit"
docker exec -i agenda_culturel-backend python3 manage.py migrate agenda_culturel
+# remove all elements in database
+echobold "Flush database"
+docker exec -i agenda_culturel-backend python3 manage.py flush --no-input
+
# import data
echobold "Import data"
docker exec -i agenda_culturel-backend python3 manage.py loaddata --format=json $FFILE
@@ -85,7 +89,4 @@ git checkout main
echobold "Update database"
docker exec -i agenda_culturel-backend python3 manage.py migrate agenda_culturel
-# create superuser
-echobold "Create superuser"
-docker exec -ti agenda_culturel-backend python3 manage.py createsuperuser
diff --git a/src/agenda_culturel/admin.py b/src/agenda_culturel/admin.py
index 9de4a23..8c5d32a 100644
--- a/src/agenda_culturel/admin.py
+++ b/src/agenda_culturel/admin.py
@@ -9,7 +9,7 @@ from .models import (
BatchImportation,
RecurrentImport,
Place,
- ContactMessage,
+ Message,
ReferenceLocation,
Organisation
)
@@ -25,7 +25,7 @@ admin.site.register(DuplicatedEvents)
admin.site.register(BatchImportation)
admin.site.register(RecurrentImport)
admin.site.register(Place)
-admin.site.register(ContactMessage)
+admin.site.register(Message)
admin.site.register(ReferenceLocation)
admin.site.register(Organisation)
diff --git a/src/agenda_culturel/calendar.py b/src/agenda_culturel/calendar.py
index a62e9ac..aa110f6 100644
--- a/src/agenda_culturel/calendar.py
+++ b/src/agenda_culturel/calendar.py
@@ -117,6 +117,23 @@ class DayInCalendar:
if e.start_time is None
else e.start_time
)
+ self.today_night = False
+ if self.is_today():
+ self.today_night = True
+ now = timezone.now()
+ nday = now.date()
+ ntime = now.time()
+ found = False
+ for idx,e in enumerate(self.events):
+ if (nday < e.start_day) or (nday == e.start_day and e.start_time and ntime <= e.start_time):
+ self.events[idx].is_first_after_now = True
+ found = True
+ break
+ if found:
+ self.today_night = False
+
+ def is_today_after_events(self):
+ return self.is_today() and self.today_night
def events_by_category_ordered(self):
from .models import Category
@@ -175,12 +192,13 @@ class IntervalInDay(DayInCalendar):
self.id = self.id + '-' + str(id)
class CalendarList:
- def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=None):
+ def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=None, qs=None):
self.firstdate = firstdate
self.lastdate = lastdate
self.now = date.today()
self.filter = filter
self.ignore_dup = ignore_dup
+ self.qs = qs
if exact:
self.c_firstdate = self.firstdate
@@ -219,9 +237,12 @@ class CalendarList:
def fill_calendar_days(self):
if self.filter is None:
- from .models import Event
+ if self.qs is None:
+ from .models import Event
- qs = Event.objects.all()
+ qs = Event.objects.all()
+ else:
+ qs = self.qs
else:
qs = self.filter.qs
@@ -229,7 +250,7 @@ class CalendarList:
qs = qs.exclude(other_versions=self.ignore_dup)
startdatetime = timezone.make_aware(datetime.combine(self.c_firstdate, time.min), timezone.get_default_timezone())
lastdatetime = timezone.make_aware(datetime.combine(self.c_lastdate, time.max), timezone.get_default_timezone())
- self.events = qs.filter(
+ qs = qs.filter(
(Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime))
| (
Q(recurrence_dtend__isnull=False)
@@ -243,7 +264,10 @@ class CalendarList:
Q(other_versions__isnull=True) |
Q(other_versions__representative=F('pk')) |
Q(other_versions__representative__isnull=True)
- ).order_by("start_time", "title__unaccent__lower").select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative")
+ ).order_by("start_time", "title__unaccent__lower")
+
+ qs = qs.select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative")
+ self.events = qs
firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
@@ -291,14 +315,14 @@ class CalendarList:
def time_intervals_list_first(self):
return self.time_intervals_list(True)
- def export_to_ics(self):
+ def export_to_ics(self, request):
from .models import Event
events = [event for day in self.get_calendar_days().values() for event in day.events]
- return Event.export_to_ics(events)
+ return Event.export_to_ics(events, request)
class CalendarMonth(CalendarList):
- def __init__(self, year, month, filter):
+ def __init__(self, year, month, filter, qs=None):
self.year = year
self.month = month
r = calendar.monthrange(year, month)
@@ -306,7 +330,7 @@ class CalendarMonth(CalendarList):
first = date(year, month, 1)
last = date(year, month, r[1])
- super().__init__(first, last, filter)
+ super().__init__(first, last, filter, qs)
def get_month_name(self):
return self.firstdate.strftime("%B")
@@ -319,14 +343,14 @@ class CalendarMonth(CalendarList):
class CalendarWeek(CalendarList):
- def __init__(self, year, week, filter):
+ def __init__(self, year, week, filter, qs=None):
self.year = year
self.week = week
first = date.fromisocalendar(self.year, self.week, 1)
last = date.fromisocalendar(self.year, self.week, 7)
- super().__init__(first, last, filter)
+ super().__init__(first, last, filter, qs)
def next_week(self):
return self.firstdate + timedelta(days=7)
@@ -336,8 +360,8 @@ class CalendarWeek(CalendarList):
class CalendarDay(CalendarList):
- def __init__(self, date, filter=None):
- super().__init__(date, date, filter, exact=True)
+ def __init__(self, date, filter=None, qs=None):
+ super().__init__(date, date, filter=filter, qs=qs, exact=True)
def get_events(self):
return self.calendar_days_list()[0].events
diff --git a/src/agenda_culturel/celery.py b/src/agenda_culturel/celery.py
index b435900..a8471a8 100644
--- a/src/agenda_culturel/celery.py
+++ b/src/agenda_culturel/celery.py
@@ -6,7 +6,8 @@ from celery.schedules import crontab
from celery.utils.log import get_task_logger
from celery.exceptions import MaxRetriesExceededError
import time as time_
-
+from django.conf import settings
+from celery.signals import worker_ready
from contextlib import contextmanager
@@ -147,6 +148,8 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
extractor = c3c.CExtractor()
elif rimport.processor == RecurrentImport.PROCESSOR.ARACHNEE:
extractor = arachnee.CExtractor()
+ elif rimport.processor == RecurrentImport.PROCESSOR.LERIO:
+ extractor = lerio.CExtractor()
else:
extractor = None
@@ -173,6 +176,11 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
published=published,
)
+ # force location if required
+ if rimport.forceLocation and location:
+ for i, e in enumerate(events['events']):
+ events['events'][i]["location"] = location
+
# convert it to json
json_events = json.dumps(events, default=str)
@@ -248,6 +256,23 @@ def daily_imports(self):
run_recurrent_imports_from_list([imp.pk for imp in imports])
+SCREENSHOT_FILE = settings.MEDIA_ROOT + '/screenshot.png'
+
+@app.task(bind=True)
+def screenshot(self):
+ downloader = ChromiumHeadlessDownloader(noimage=False)
+ downloader.screenshot("https://pommesdelune.fr", SCREENSHOT_FILE)
+
+@worker_ready.connect
+def at_start(sender, **k):
+ if not os.path.isfile(SCREENSHOT_FILE):
+ logger.info("Init screenshot file")
+ with sender.app.connection() as conn:
+ sender.app.send_task('agenda_culturel.celery.screenshot', None, connection=conn)
+ else:
+ logger.info("Screenshot file already exists")
+
+
@app.task(bind=True)
def run_all_recurrent_imports(self):
from agenda_culturel.models import RecurrentImport
@@ -289,7 +314,7 @@ def weekly_imports(self):
run_recurrent_imports_from_list([imp.pk for imp in imports])
@app.task(base=ChromiumTask, bind=True)
-def import_events_from_url(self, url, cat, tags, force=False):
+def import_events_from_url(self, url, cat, tags, force=False, user_id=None):
from .db_importer import DBImporterEvents
from agenda_culturel.models import RecurrentImport, BatchImportation
from agenda_culturel.models import Event, Category
@@ -298,7 +323,7 @@ def import_events_from_url(self, url, cat, tags, force=False):
if acquired:
- logger.info("URL import: {}".format(self.request.id))
+ logger.info("URL import: {}".format(self.request.id) + " force " + str(force))
# clean url
@@ -335,7 +360,7 @@ def import_events_from_url(self, url, cat, tags, force=False):
json_events = json.dumps(events, default=str)
# import events (from json)
- success, error_message = importer.import_events(json_events)
+ success, error_message = importer.import_events(json_events, user_id)
# finally, close task
close_import_task(self.request.id, success, error_message, importer)
@@ -352,14 +377,14 @@ def import_events_from_url(self, url, cat, tags, force=False):
@app.task(base=ChromiumTask, bind=True)
-def import_events_from_urls(self, urls_cat_tags):
+def import_events_from_urls(self, urls_cat_tags, user_id=None):
for ucat in urls_cat_tags:
if ucat is not None:
url = ucat[0]
cat = ucat[1]
tags = ucat[2]
- import_events_from_url.delay(url, cat, tags)
+ import_events_from_url.delay(url, cat, tags, user_id=user_id)
app.conf.beat_schedule = {
@@ -368,6 +393,10 @@ app.conf.beat_schedule = {
# Daily imports at 3:14 a.m.
"schedule": crontab(hour=3, minute=14),
},
+ "daily_screenshot": {
+ "task": "agenda_culturel.celery.screenshot",
+ "schedule": crontab(hour=3, minute=3),
+ },
"weekly_imports": {
"task": "agenda_culturel.celery.weekly_imports",
# Daily imports on Mondays at 2:22 a.m.
diff --git a/src/agenda_culturel/db_importer.py b/src/agenda_culturel/db_importer.py
index de38ce8..24e9c22 100644
--- a/src/agenda_culturel/db_importer.py
+++ b/src/agenda_culturel/db_importer.py
@@ -11,6 +11,7 @@ class DBImporterEvents:
def __init__(self, celery_id):
self.celery_id = celery_id
self.error_message = ""
+ self.user_id = None
self.init_result_properties()
self.today = timezone.now().date().isoformat()
@@ -34,9 +35,10 @@ class DBImporterEvents:
def get_nb_removed_events(self):
return self.nb_removed
- def import_events(self, json_structure):
+ def import_events(self, json_structure, user_id=None):
print(json_structure)
self.init_result_properties()
+ self.user_id = user_id
try:
structure = json.loads(json_structure)
@@ -95,7 +97,7 @@ class DBImporterEvents:
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.event_objects, remove_missing_from_source=self.url, user_id=self.user_id
)
def is_valid_event_structure(self, event):
diff --git a/src/agenda_culturel/filters.py b/src/agenda_culturel/filters.py
index 07c23e8..bb88335 100644
--- a/src/agenda_culturel/filters.py
+++ b/src/agenda_culturel/filters.py
@@ -44,7 +44,7 @@ from .models import (
Tag,
Event,
Category,
- ContactMessage,
+ Message,
DuplicatedEvents
)
@@ -137,7 +137,11 @@ class EventFilter(django_filters.FilterSet):
if self.get_cleaned_data("position") is None or self.get_cleaned_data("radius") is None:
return parent
d = self.get_cleaned_data("radius")
- p = self.get_cleaned_data("position").location
+ p = self.get_cleaned_data("position")
+ if not isinstance(d, str) or not isinstance(p, ReferenceLocation):
+ return parent
+ p = p.location
+
return parent.exclude(exact_location=False).filter(exact_location__location__distance_lt=(p, D(km=d)))
def get_url(self):
@@ -188,6 +192,7 @@ class EventFilter(django_filters.FilterSet):
def get_cleaned_data(self, name):
+
try:
return self.form.cleaned_data[name]
except AttributeError:
@@ -356,7 +361,7 @@ class EventFilterAdmin(django_filters.FilterSet):
fields = ["status"]
-class ContactMessagesFilterAdmin(django_filters.FilterSet):
+class MessagesFilterAdmin(django_filters.FilterSet):
closed = django_filters.MultipleChoiceFilter(
label="Status",
choices=((True, _("Closed")), (False, _("Open"))),
@@ -369,7 +374,7 @@ class ContactMessagesFilterAdmin(django_filters.FilterSet):
)
class Meta:
- model = ContactMessage
+ model = Message
fields = ["closed", "spam"]
diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms.py
index f93b4db..d5cf26b 100644
--- a/src/agenda_culturel/forms.py
+++ b/src/agenda_culturel/forms.py
@@ -16,6 +16,7 @@ from django.forms import (
)
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
+from .utils import PlaceGuesser
from .models import (
Event,
RecurrentImport,
@@ -23,7 +24,7 @@ from .models import (
Place,
Category,
Tag,
- ContactMessage
+ Message
)
from django.conf import settings
from django.core.files import File
@@ -32,7 +33,6 @@ from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc
from django.utils.safestring import mark_safe
-from django.utils.timezone import localtime
from django.utils.formats import localize
from .templatetags.event_extra import event_field_verbose_name, field_to_html
import os
@@ -74,7 +74,7 @@ class GroupFormMixin:
return [f for f in self.visible_fields() if not hasattr(f.field, "toggle_group") and (not hasattr(f.field, "group_id") or f.field.group_id == None)]
def fields_by_group(self):
- return [(g, self.get_fields_in_group(g)) for g in self.groups] + [(None, self.get_no_group_fields())]
+ return [(g, self.get_fields_in_group(g)) for g in self.groups] + [(GroupFormMixin.FieldGroup("other", _("Other")), self.get_no_group_fields())]
def clean(self):
result = super().clean()
@@ -189,6 +189,7 @@ class EventForm(GroupFormMixin, ModelForm):
old_local_image = CharField(widget=HiddenInput(), required=False)
simple_cloning = CharField(widget=HiddenInput(), required=False)
+ cloning = CharField(widget=HiddenInput(), required=False)
tags = MultipleChoiceField(
label=_("Tags"),
@@ -205,7 +206,11 @@ class EventForm(GroupFormMixin, ModelForm):
"modified_date",
"moderated_date",
"import_sources",
- "image"
+ "image",
+ "moderated_by_user",
+ "modified_by_user",
+ "created_by_user",
+ "imported_by_user"
]
widgets = {
"start_day": TextInput(
@@ -363,6 +368,9 @@ class EventModerateForm(ModelForm):
"exact_location",
"tags"
]
+ widgets = {
+ "status": RadioSelect
+ }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -493,7 +501,7 @@ class SelectEventInList(Form):
super().__init__(*args, **kwargs)
self.fields["event"].choices = [
- (e.pk, str(e.start_day) + " " + e.title + ((", " + e.location) if e.location else "")) for e in events
+ (e.pk, (e.start_time.strftime('%H:%M') + " : " if e.start_time else "") + e.title + ((", " + e.location) if e.location else "")) for e in events
]
@@ -541,17 +549,17 @@ class MergeDuplicates(Form):
'
' + e.title + ""
)
result += (
- "Création : " + localize(localtime(e.created_date)) + ""
+ "Création : " + localize(e.created_date) + ""
)
result += (
"Dernière modification : "
- + localize(localtime(e.modified_date))
+ + localize(e.modified_date)
+ ""
)
if e.imported_date:
result += (
"Dernière importation : "
- + localize(localtime(e.imported_date))
+ + localize(e.imported_date)
+ ""
)
result += ""
@@ -602,7 +610,7 @@ class MergeDuplicates(Form):
result += ''
+ result = ('
'
- )
+ + '''
''')
+
+ return mark_safe(result)
def apply(self):
return self.cleaned_data.get("apply_to_all")
-class ContactMessageForm(ModelForm):
+class MessageForm(ModelForm):
class Meta:
- model = ContactMessage
+ model = Message
fields = ["subject", "name", "email", "message", "related_event"]
- widgets = {"related_event": HiddenInput()}
+ widgets = {"related_event": HiddenInput(), "user": HiddenInput() }
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event", False)
+ self.internal = kwargs.pop("internal", False)
super().__init__(*args, **kwargs)
+ self.fields['related_event'].required = False
+ if self.internal:
+ self.fields.pop("name")
+ self.fields.pop("email")
+
+class MessageEventForm(ModelForm):
+
+ class Meta:
+ model = Message
+ fields = ["message"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["message"].label = _("Add a comment")
\ No newline at end of file
diff --git a/src/agenda_culturel/import_tasks/custom_extractors/fbevents.py b/src/agenda_culturel/import_tasks/custom_extractors/fbevents.py
index b9e3b32..d6be501 100644
--- a/src/agenda_culturel/import_tasks/custom_extractors/fbevents.py
+++ b/src/agenda_culturel/import_tasks/custom_extractors/fbevents.py
@@ -3,6 +3,12 @@ from ..extractor_facebook import FacebookEvent
import json5
from bs4 import BeautifulSoup
import json
+import os
+from datetime import datetime
+
+import logging
+
+logger = logging.getLogger(__name__)
# A class dedicated to get events from a facebook events page
@@ -13,10 +19,27 @@ class CExtractor(TwoStepsExtractor):
def build_event_url_list(self, content):
soup = BeautifulSoup(content, "html.parser")
+ debug = False
+
+ found = False
links = soup.find_all("a")
for link in links:
if link.get("href").startswith('https://www.facebook.com/events/'):
self.add_event_url(link.get('href').split('?')[0])
+ found = True
+
+ if not found and debug:
+ directory = "errors/"
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ now = datetime.now()
+ filename = directory + now.strftime("%Y%m%d_%H%M%S") + ".html"
+ logger.warning("cannot find any event link in events page. Save content page in " + filename)
+ with open(filename, "w") as text_file:
+ text_file.write("\n\n")
+ text_file.write(content)
+
+
def add_event_from_content(
@@ -42,4 +65,7 @@ class CExtractor(TwoStepsExtractor):
event["published"] = published
self.add_event(default_values, **event)
+ else:
+ logger.warning("cannot find any event in page")
+
diff --git a/src/agenda_culturel/import_tasks/custom_extractors/lerio.py b/src/agenda_culturel/import_tasks/custom_extractors/lerio.py
new file mode 100644
index 0000000..117693c
--- /dev/null
+++ b/src/agenda_culturel/import_tasks/custom_extractors/lerio.py
@@ -0,0 +1,91 @@
+from ..generic_extractors import *
+from bs4 import BeautifulSoup
+from datetime import datetime
+
+# A class dedicated to get events from Cinéma Le Rio (Clermont-Ferrand)
+# URL: https://www.cinemalerio.com/evenements/
+class CExtractor(TwoStepsExtractorNoPause):
+
+ def __init__(self):
+ super().__init__()
+ self.possible_dates = {}
+ self.theater = None
+
+ def build_event_url_list(self, content, infuture_days=180):
+
+ soup = BeautifulSoup(content, "html.parser")
+
+ links = soup.select("td.seance_link a")
+ if links:
+ for l in links:
+ print(l["href"])
+ self.add_event_url(l["href"])
+
+ def to_text_select_one(soup, filter):
+ e = soup.select_one(filter)
+ if e is None:
+ return None
+ else:
+ return e.text
+
+ 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.select_one("h1").text
+
+ alerte_date = CExtractor.to_text_select_one(soup, ".alerte_date")
+ if alerte_date is None:
+ return
+ dh = alerte_date.split("à")
+ # if date is not found, we skip
+ if len(dh) != 2:
+ return
+
+ date = Extractor.parse_french_date(dh[0], default_year=datetime.now().year)
+ time = Extractor.parse_french_time(dh[1])
+
+ synopsis = CExtractor.to_text_select_one(soup, ".synopsis_bloc")
+ special_titre = CExtractor.to_text_select_one(soup, ".alerte_titre")
+ special = CExtractor.to_text_select_one(soup, ".alerte_text")
+
+ # it's not a specific event: we skip it
+ special_lines = None if special is None else special.split('\n')
+ if special is None or len(special_lines) == 0 or \
+ (len(special_lines) == 1 and special_lines[0].strip().startswith('En partenariat')):
+ return
+
+ description = "\n\n".join([x for x in [synopsis, special_titre, special] if not x is None])
+
+ image = soup.select_one(".col1 img")
+ image_alt = None
+ if not image is None:
+ image_alt = image["alt"]
+ image = image["src"]
+
+ self.add_event_with_props(
+ default_values,
+ event_url,
+ title,
+ None,
+ date,
+ None,
+ description,
+ [],
+ recurrences=None,
+ uuids=[event_url],
+ url_human=event_url,
+ start_time=time,
+ end_day=None,
+ end_time=None,
+ published=published,
+ image=image,
+ image_alt=image_alt
+ )
\ No newline at end of file
diff --git a/src/agenda_culturel/import_tasks/downloader.py b/src/agenda_culturel/import_tasks/downloader.py
index 7fd45ee..905c130 100644
--- a/src/agenda_culturel/import_tasks/downloader.py
+++ b/src/agenda_culturel/import_tasks/downloader.py
@@ -66,7 +66,7 @@ class SimpleDownloader(Downloader):
class ChromiumHeadlessDownloader(Downloader):
- def __init__(self, pause=True):
+ def __init__(self, pause=True, noimage=True):
super().__init__()
self.pause = pause
self.options = Options()
@@ -78,17 +78,31 @@ class ChromiumHeadlessDownloader(Downloader):
self.options.add_argument("--disable-dev-shm-usage")
self.options.add_argument("--disable-browser-side-navigation")
self.options.add_argument("--disable-gpu")
- self.options.add_experimental_option(
- "prefs", {
- # block image loading
- "profile.managed_default_content_settings.images": 2,
- }
- )
+ if noimage:
+ self.options.add_experimental_option(
+ "prefs", {
+ # block image loading
+ "profile.managed_default_content_settings.images": 2,
+ }
+ )
self.service = Service("/usr/bin/chromedriver")
self.driver = webdriver.Chrome(service=self.service, options=self.options)
+ def screenshot(self, url, path_image):
+ print("Screenshot {}".format(url))
+ try:
+ self.driver.get(url)
+ if self.pause:
+ time.sleep(2)
+ self.driver.save_screenshot(path_image)
+ except:
+ print(f">> Exception: {URL}")
+ return False
+
+ return True
+
def download(self, url, referer=None, post=None):
if post:
raise Exception("POST method with Chromium headless not yet implemented")
diff --git a/src/agenda_culturel/import_tasks/extractor.py b/src/agenda_culturel/import_tasks/extractor.py
index 3e75870..3ce362b 100644
--- a/src/agenda_culturel/import_tasks/extractor.py
+++ b/src/agenda_culturel/import_tasks/extractor.py
@@ -49,7 +49,7 @@ class Extractor(ABC):
return i + 1
return None
- def parse_french_date(text):
+ def parse_french_date(text, default_year=None):
# format NomJour Numero Mois Année
m = re.search(
"[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text
@@ -73,8 +73,15 @@ class Extractor(ABC):
month = int(m.group(2))
year = m.group(3)
else:
- # TODO: consolider les cas non satisfaits
- return None
+ # format Numero Mois Annee
+ m = re.search("([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)", text)
+ if m:
+ day = m.group(1)
+ month = Extractor.guess_month(m.group(2))
+ year = default_year
+ else:
+ # TODO: consolider les cas non satisfaits
+ return None
if month is None:
return None
@@ -254,9 +261,9 @@ class EventNotFoundExtractor(Extractor):
self.set_header(url)
self.clear_events()
- self.add_event(default_values, "événement sans titre",
+ self.add_event(default_values, "événement sans titre depuis " + url,
None, timezone.now().date(), None,
- "l'import a échoué, la saisie doit se faire manuellement à partir de l'url source",
+ "l'import a échoué, la saisie doit se faire manuellement à partir de l'url source " + url,
[], [url], published=False, url_human=url)
return self.get_structure()
diff --git a/src/agenda_culturel/import_tasks/extractor_facebook.py b/src/agenda_culturel/import_tasks/extractor_facebook.py
index e0c50b6..b7970ab 100644
--- a/src/agenda_culturel/import_tasks/extractor_facebook.py
+++ b/src/agenda_culturel/import_tasks/extractor_facebook.py
@@ -239,7 +239,7 @@ class FacebookEventExtractor(Extractor):
result = "https://www.facebook.com" + u.path
# remove name in the url
- match = re.match(r"(.*/events)/s/([a-zA-Z-][a-zA-Z-0-9]+)/([0-9/]*)", result)
+ match = re.match(r"(.*/events)/s/([a-zA-Z-][a-zA-Z-0-9-]+)/([0-9/]*)", result)
if match:
result = match[1] + "/" + match[3]
diff --git a/src/agenda_culturel/import_tasks/generic_extractors.py b/src/agenda_culturel/import_tasks/generic_extractors.py
index 10803c9..14acc44 100644
--- a/src/agenda_culturel/import_tasks/generic_extractors.py
+++ b/src/agenda_culturel/import_tasks/generic_extractors.py
@@ -264,7 +264,10 @@ class TwoStepsExtractorNoPause(TwoStepsExtractor):
only_future=True,
ignore_404=True
):
- pause = self.downloader.pause
+ if hasattr(self.downloader, "pause"):
+ pause = self.downloader.pause
+ else:
+ pause = False
self.downloader.pause = False
result = super().extract(content, url, url_human, default_values, published, only_future, ignore_404)
self.downloader.pause = pause
diff --git a/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po b/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po
index 20cb064..a315b39 100644
--- a/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po
+++ b/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: agenda_culturel\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-11-28 00:09+0100\n"
+"POT-Creation-Date: 2024-12-22 15:52+0100\n"
"PO-Revision-Date: 2023-10-29 14:16+0000\n"
"Last-Translator: Jean-Marie Favreau \n"
"Language-Team: Jean-Marie Favreau \n"
@@ -17,76 +17,76 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: agenda_culturel/calendar.py:151
+#: agenda_culturel/calendar.py:168
msgid "All day today"
msgstr "Aujourd'hui toute la journée"
-#: agenda_culturel/calendar.py:152
+#: agenda_culturel/calendar.py:169
msgid "This morning"
msgstr "Ce matin"
-#: agenda_culturel/calendar.py:152
+#: agenda_culturel/calendar.py:169
msgid "This noon"
msgstr "Ce midi"
-#: agenda_culturel/calendar.py:152
+#: agenda_culturel/calendar.py:169
msgid "This afternoon"
msgstr "Cet après-midi"
-#: agenda_culturel/calendar.py:152
+#: agenda_culturel/calendar.py:169
msgid "This evening"
msgstr "Ce soir"
-#: agenda_culturel/calendar.py:154
+#: agenda_culturel/calendar.py:171
msgid "Tomorrow"
msgstr "Demain"
-#: agenda_culturel/calendar.py:155
+#: agenda_culturel/calendar.py:172
msgid "All day tomorrow"
msgstr "Toute la journée de demain"
-#: agenda_culturel/calendar.py:156 agenda_culturel/calendar.py:160
+#: agenda_culturel/calendar.py:173 agenda_culturel/calendar.py:177
#, python-format
msgid "%s morning"
msgstr "%s matin"
-#: agenda_culturel/calendar.py:156 agenda_culturel/calendar.py:160
+#: agenda_culturel/calendar.py:173 agenda_culturel/calendar.py:177
#, python-format
msgid "%s noon"
msgstr "%s midi"
-#: agenda_culturel/calendar.py:156 agenda_culturel/calendar.py:160
+#: agenda_culturel/calendar.py:173 agenda_culturel/calendar.py:177
#, python-format
msgid "%s afternoon"
msgstr "%s après-midi"
-#: agenda_culturel/calendar.py:156 agenda_culturel/calendar.py:160
+#: agenda_culturel/calendar.py:173 agenda_culturel/calendar.py:177
#, python-format
msgid "%s evening"
msgstr "%s soir"
-#: agenda_culturel/calendar.py:159
+#: agenda_culturel/calendar.py:176
#, python-format
msgid "All day %s"
msgstr "Toute la journée de %s"
-#: agenda_culturel/calendar.py:161
+#: agenda_culturel/calendar.py:178
msgid "All day"
msgstr "Toute la journée"
-#: agenda_culturel/calendar.py:162
+#: agenda_culturel/calendar.py:179
msgid "Morning"
msgstr "Matin"
-#: agenda_culturel/calendar.py:162
+#: agenda_culturel/calendar.py:179
msgid "Noon"
msgstr "Midi"
-#: agenda_culturel/calendar.py:162
+#: agenda_culturel/calendar.py:179
msgid "Afternoon"
msgstr "Après-midi"
-#: agenda_culturel/calendar.py:162
+#: agenda_culturel/calendar.py:179
msgid "Evening"
msgstr "Soir"
@@ -94,42 +94,46 @@ msgstr "Soir"
msgid "Select a location"
msgstr "Choisir une localité"
-#: agenda_culturel/filters.py:326
+#: agenda_culturel/filters.py:331
msgid "Representative version"
msgstr "Version représentative"
-#: agenda_culturel/filters.py:327
+#: agenda_culturel/filters.py:332
msgid "Yes"
msgstr "Oui"
-#: agenda_culturel/filters.py:327
+#: agenda_culturel/filters.py:332
msgid "Non"
msgstr "Non"
-#: agenda_culturel/filters.py:332
+#: agenda_culturel/filters.py:337
msgid "Imported from"
msgstr "Importé depuis"
-#: agenda_culturel/filters.py:362 agenda_culturel/models.py:1728
+#: agenda_culturel/filters.py:367 agenda_culturel/models.py:1852
msgid "Closed"
msgstr "Fermé"
-#: agenda_culturel/filters.py:362
+#: agenda_culturel/filters.py:367
msgid "Open"
msgstr "Ouvert"
-#: agenda_culturel/filters.py:367 agenda_culturel/models.py:1722
+#: agenda_culturel/filters.py:372 agenda_culturel/models.py:1846
msgid "Spam"
msgstr "Spam"
-#: agenda_culturel/filters.py:367
+#: agenda_culturel/filters.py:372
msgid "Non spam"
msgstr "Non spam"
-#: agenda_culturel/filters.py:378
+#: agenda_culturel/filters.py:383
msgid "Search"
msgstr "Rechercher"
+#: agenda_culturel/forms.py:77
+msgid "Other"
+msgstr "Autres"
+
#: agenda_culturel/forms.py:107
msgid "Name of new tag"
msgstr "Nom de la nouvelle étiquette"
@@ -141,68 +145,68 @@ msgstr ""
"Forcer le renommage malgré l'existence d'événements utilisant déjà "
"l'étiquette choisie."
-#: agenda_culturel/forms.py:133 agenda_culturel/models.py:175
-#: agenda_culturel/models.py:574 agenda_culturel/models.py:1843
-#: agenda_culturel/models.py:1948
+#: agenda_culturel/forms.py:133 agenda_culturel/models.py:178
+#: agenda_culturel/models.py:620 agenda_culturel/models.py:1977
+#: agenda_culturel/models.py:2087
msgid "Category"
msgstr "Catégorie"
#: agenda_culturel/forms.py:139 agenda_culturel/forms.py:164
-#: agenda_culturel/forms.py:194 agenda_culturel/forms.py:338
-#: agenda_culturel/models.py:216 agenda_culturel/models.py:682
+#: agenda_culturel/forms.py:194 agenda_culturel/forms.py:349
+#: agenda_culturel/models.py:219 agenda_culturel/models.py:728
msgid "Tags"
msgstr "Étiquettes"
-#: agenda_culturel/forms.py:246
+#: agenda_culturel/forms.py:250
msgid "Main fields"
msgstr "Champs principaux"
-#: agenda_culturel/forms.py:249
+#: agenda_culturel/forms.py:253
msgid "Start of event"
msgstr "Début de l'événement"
-#: agenda_culturel/forms.py:253
+#: agenda_culturel/forms.py:257
msgid "End of event"
msgstr "Fin de l'événement"
-#: agenda_culturel/forms.py:257
+#: agenda_culturel/forms.py:262
msgid "This is a recurring event"
msgstr "Cet événement est récurrent"
-#: agenda_culturel/forms.py:260
+#: agenda_culturel/forms.py:271
msgid "Details"
msgstr "Détails"
-#: agenda_culturel/forms.py:265 agenda_culturel/models.py:604
-#: agenda_culturel/models.py:1824
+#: agenda_culturel/forms.py:276 agenda_culturel/models.py:650
+#: agenda_culturel/models.py:1952
msgid "Location"
msgstr "Localisation"
-#: agenda_culturel/forms.py:269 agenda_culturel/models.py:637
+#: agenda_culturel/forms.py:280 agenda_culturel/models.py:683
msgid "Illustration"
msgstr "Illustration"
-#: agenda_culturel/forms.py:275 agenda_culturel/forms.py:280
+#: agenda_culturel/forms.py:286 agenda_culturel/forms.py:291
msgid "Meta information"
msgstr "Méta-informations"
-#: agenda_culturel/forms.py:295
+#: agenda_culturel/forms.py:306
msgid "The end date must be after the start date."
msgstr "La date de fin doit être après la date de début."
-#: agenda_culturel/forms.py:311
+#: agenda_culturel/forms.py:322
msgid "The end time cannot be earlier than the start time."
msgstr "L'heure de fin ne peut pas être avant l'heure de début."
-#: agenda_culturel/forms.py:339
+#: agenda_culturel/forms.py:350
msgid "Select tags from existing ones."
msgstr "Sélectionner des étiquettes depuis celles existantes."
-#: agenda_culturel/forms.py:344
+#: agenda_culturel/forms.py:355
msgid "New tags"
msgstr "Nouvelles étiquettes"
-#: agenda_culturel/forms.py:345
+#: agenda_culturel/forms.py:356
msgid ""
"Create new labels (sparingly). Note: by starting your tag with the "
"characters “TW:”, youll create a “trigger warning” tag, and the associated "
@@ -213,156 +217,176 @@ msgstr ""
"étiquette “trigger warning”, et les événements associés seront annoncés "
"comme tels."
-#: agenda_culturel/forms.py:388
+#: agenda_culturel/forms.py:402
msgid "JSON in the format expected for the import."
msgstr "JSON dans le format attendu pour l'import"
-#: agenda_culturel/forms.py:410
+#: agenda_culturel/forms.py:424
msgid " (locally modified version)"
msgstr " (version modifiée localement)"
-#: agenda_culturel/forms.py:414
+#: agenda_culturel/forms.py:428
msgid " (synchronized on import version)"
msgstr " (version synchronisée sur l'import)"
-#: agenda_culturel/forms.py:418
+#: agenda_culturel/forms.py:432
msgid "Select {} as representative version."
msgstr "Sélectionner {} comme version représentative"
-#: agenda_culturel/forms.py:427
+#: agenda_culturel/forms.py:441
msgid "Update {} using some fields from other versions (interactive mode)."
msgstr ""
"Mettre à jour {} en utilisant quelques champs des autres versions (mode "
"interactif)."
-#: agenda_culturel/forms.py:434
+#: agenda_culturel/forms.py:448
msgid " Warning: a version is already locally modified."
msgstr " Attention: une version a déjà été modifiée localement."
-#: agenda_culturel/forms.py:439
+#: agenda_culturel/forms.py:453
msgid "Create a new version by merging (interactive mode)."
msgstr "Créer une nouvelle version par fusion (mode interactif)."
-#: agenda_culturel/forms.py:446
+#: agenda_culturel/forms.py:460
msgid "Make {} independent."
msgstr "Rendre {} indépendant."
-#: agenda_culturel/forms.py:448
+#: agenda_culturel/forms.py:462
msgid "Make all versions independent."
msgstr "Rendre toutes les versions indépendantes."
-#: agenda_culturel/forms.py:482 agenda_culturel/models.py:762
+#: agenda_culturel/forms.py:496 agenda_culturel/models.py:812
msgid "Event"
msgstr "Événement"
-#: agenda_culturel/forms.py:507
+#: agenda_culturel/forms.py:521
msgid "Value of the selected version"
msgstr "Valeur de la version sélectionnée"
-#: agenda_culturel/forms.py:509 agenda_culturel/forms.py:513
+#: agenda_culturel/forms.py:523 agenda_culturel/forms.py:527
msgid "Value of version {}"
msgstr "Valeur de la version {}"
-#: agenda_culturel/forms.py:662
+#: agenda_culturel/forms.py:676
msgid "Apply category {} to the event {}"
msgstr "Appliquer la catégorie {} à l'événement {}"
-#: agenda_culturel/forms.py:679 agenda_culturel/models.py:458
-#: agenda_culturel/models.py:2000
+#: agenda_culturel/forms.py:693 agenda_culturel/models.py:466
+#: agenda_culturel/models.py:2139
msgid "Place"
msgstr "Lieu"
-#: agenda_culturel/forms.py:681
+#: agenda_culturel/forms.py:695
msgid "Create a missing place"
msgstr "Créer un lieu manquant"
-#: agenda_culturel/forms.py:691
+#: agenda_culturel/forms.py:705
msgid "Add \"{}\" to the aliases of the place"
msgstr "Ajouter « {} » aux alias du lieu"
-#: agenda_culturel/forms.py:720
+#: agenda_culturel/forms.py:734
msgid "On saving, use aliases to detect all matching events with missing place"
msgstr ""
"Lors de l'enregistrement, utiliser des alias pour détecter tous les "
"événements correspondants dont la place est manquante."
-#: agenda_culturel/models.py:56 agenda_culturel/models.py:104
-#: agenda_culturel/models.py:185 agenda_culturel/models.py:402
-#: agenda_culturel/models.py:430 agenda_culturel/models.py:511
-#: agenda_culturel/models.py:1704 agenda_culturel/models.py:1778
+#: agenda_culturel/forms.py:747
+msgid "Header"
+msgstr "Entête"
+
+#: agenda_culturel/forms.py:751 agenda_culturel/models.py:439
+msgid "Address"
+msgstr "Adresse"
+
+#: agenda_culturel/forms.py:757
+msgid "Meta"
+msgstr "Méta"
+
+#: agenda_culturel/forms.py:760
+msgid "Information"
+msgstr "Informations"
+
+#: agenda_culturel/forms.py:810
+msgid "Add a comment"
+msgstr "Ajouter un commentaire"
+
+#: agenda_culturel/models.py:59 agenda_culturel/models.py:107
+#: agenda_culturel/models.py:188 agenda_culturel/models.py:409
+#: agenda_culturel/models.py:437 agenda_culturel/models.py:524
+#: agenda_culturel/models.py:1828 agenda_culturel/models.py:1906
msgid "Name"
msgstr "Nom"
-#: agenda_culturel/models.py:57 agenda_culturel/models.py:104
+#: agenda_culturel/models.py:60 agenda_culturel/models.py:107
msgid "Category name"
msgstr "Nom de la catégorie"
-#: agenda_culturel/models.py:62
+#: agenda_culturel/models.py:65
msgid "Content"
msgstr "Contenu"
-#: agenda_culturel/models.py:62
+#: agenda_culturel/models.py:65
msgid "Text as shown to the visitors"
msgstr "Texte tel que présenté aux visiteureuses"
-#: agenda_culturel/models.py:66
+#: agenda_culturel/models.py:69
msgid "URL path"
msgstr "Chemin URL"
-#: agenda_culturel/models.py:67
+#: agenda_culturel/models.py:70
msgid "URL path where the content is included."
msgstr "Chemin URL où le contenu est présent."
-#: agenda_culturel/models.py:71
+#: agenda_culturel/models.py:74
msgid "Static content"
msgstr "Contenu statique"
-#: agenda_culturel/models.py:72
+#: agenda_culturel/models.py:75
msgid "Static contents"
msgstr "Contenus statiques"
-#: agenda_culturel/models.py:108
+#: agenda_culturel/models.py:111
msgid "Color"
msgstr "Couleur"
-#: agenda_culturel/models.py:109
+#: agenda_culturel/models.py:112
msgid "Color used as background for the category"
msgstr "Couleur utilisée comme fond de la catégorie"
-#: agenda_culturel/models.py:115
+#: agenda_culturel/models.py:118
msgid "Pictogram"
msgstr "Pictogramme"
-#: agenda_culturel/models.py:116
+#: agenda_culturel/models.py:119
msgid "Pictogram of the category (svg format)"
msgstr "Pictogramme de la catégorie (format svg)"
-#: agenda_culturel/models.py:123
+#: agenda_culturel/models.py:126
msgid "Position for ordering categories"
msgstr "Position pour ordonner les catégories"
-#: agenda_culturel/models.py:176
+#: agenda_culturel/models.py:179
msgid "Categories"
msgstr "Catégories"
-#: agenda_culturel/models.py:185
+#: agenda_culturel/models.py:188
msgid "Tag name"
msgstr "Nom de l'étiquette"
-#: agenda_culturel/models.py:190 agenda_culturel/models.py:441
-#: agenda_culturel/models.py:523 agenda_culturel/models.py:621
+#: agenda_culturel/models.py:193 agenda_culturel/models.py:449
+#: agenda_culturel/models.py:536 agenda_culturel/models.py:667
msgid "Description"
msgstr "Description"
-#: agenda_culturel/models.py:191
+#: agenda_culturel/models.py:194
msgid "Description of the tag"
msgstr "Description de l'étiquette"
-#: agenda_culturel/models.py:197
+#: agenda_culturel/models.py:200
msgid "Principal"
msgstr "Principal"
-#: agenda_culturel/models.py:198
+#: agenda_culturel/models.py:201
msgid ""
"This tag is highlighted as a main tag for visitors, particularly in the "
"filter."
@@ -370,107 +394,115 @@ msgstr ""
"Cette étiquette est mise en avant comme étiquette principale pour les "
"visiteurs, en particulier dans le filtre."
-#: agenda_culturel/models.py:203
+#: agenda_culturel/models.py:206
msgid "In excluded suggestions"
msgstr "Dans les suggestions d'exclusion"
-#: agenda_culturel/models.py:204
+#: agenda_culturel/models.py:207
msgid "This tag will be part of the excluded suggestions."
msgstr "Cette étiquette fera partie des suggestions d'exclusion."
-#: agenda_culturel/models.py:209
+#: agenda_culturel/models.py:212
msgid "In included suggestions"
msgstr "Dans les suggestions d'inclusion."
-#: agenda_culturel/models.py:210
+#: agenda_culturel/models.py:213
msgid "This tag will be part of the included suggestions."
msgstr "Cette étiquette fera partie des suggestions d'inclusion."
-#: agenda_culturel/models.py:215
+#: agenda_culturel/models.py:218
msgid "Tag"
msgstr "Étiquette"
-#: agenda_culturel/models.py:261
+#: agenda_culturel/models.py:264
msgid "Suggestions"
msgstr "Suggestions"
-#: agenda_culturel/models.py:262
+#: agenda_culturel/models.py:265
msgid "Others"
msgstr "Autres"
-#: agenda_culturel/models.py:277
+#: agenda_culturel/models.py:280
msgid "Representative event"
msgstr "Événement représentatif"
-#: agenda_culturel/models.py:278
+#: agenda_culturel/models.py:281
msgid "This event is the representative event of the duplicated events group"
msgstr ""
"Cet événement est l'événement représentatif du groupe d'événements dupliqués."
-#: agenda_culturel/models.py:285 agenda_culturel/models.py:286
+#: agenda_culturel/models.py:288 agenda_culturel/models.py:289
msgid "Duplicated events"
msgstr "Événements dupliqués"
-#: agenda_culturel/models.py:402
+#: agenda_culturel/models.py:409
msgid "Name of the location"
msgstr "Nom de la position"
-#: agenda_culturel/models.py:405
+#: agenda_culturel/models.py:412
msgid "Main"
msgstr "Principale"
-#: agenda_culturel/models.py:406
+#: agenda_culturel/models.py:413
msgid "This location is one of the main locations (shown first higher values)."
msgstr ""
"Cette position est une position principale (affichage en premier des plus "
"grandes valeurs)."
-#: agenda_culturel/models.py:410
+#: agenda_culturel/models.py:417
msgid "Suggested distance (km)"
msgstr ""
-#: agenda_culturel/models.py:411
+#: agenda_culturel/models.py:418
msgid ""
"If this distance is given, this location is part of the suggested filters."
msgstr ""
-#: agenda_culturel/models.py:417
+#: agenda_culturel/models.py:424
msgid "Reference location"
msgstr "Position de référence"
-#: agenda_culturel/models.py:418
+#: agenda_culturel/models.py:425
msgid "Reference locations"
msgstr "Positions de référence"
-#: agenda_culturel/models.py:430
+#: agenda_culturel/models.py:437
msgid "Name of the place"
msgstr "Nom du lieu"
-#: agenda_culturel/models.py:432
-msgid "Address"
-msgstr "Adresse"
-
-#: agenda_culturel/models.py:433
+#: agenda_culturel/models.py:440
msgid "Address of this place (without city name)"
msgstr "Adresse de ce lieu (sans le nom de la ville)"
-#: agenda_culturel/models.py:437
+#: agenda_culturel/models.py:444
+msgid "Postcode"
+msgstr "Code postal"
+
+#: agenda_culturel/models.py:444
+msgid ""
+"The post code is not displayed, but makes it easier to find an address when "
+"you enter it."
+msgstr ""
+"Le code postal ne sera pas affiché, mais facilite la recherche d'adresse au "
+"moment de la saisie."
+
+#: agenda_culturel/models.py:445
msgid "City"
msgstr "Ville"
-#: agenda_culturel/models.py:437
+#: agenda_culturel/models.py:445
msgid "City name"
msgstr "Nom de la ville"
-#: agenda_culturel/models.py:442
+#: agenda_culturel/models.py:450
msgid "Description of the place, including accessibility."
msgstr "Description du lieu, inclus l'accessibilité."
-#: agenda_culturel/models.py:449
+#: agenda_culturel/models.py:457
msgid "Alternative names"
msgstr "Noms alternatifs"
-#: agenda_culturel/models.py:451
+#: agenda_culturel/models.py:459
msgid ""
"Alternative names or addresses used to match a place with the free-form "
"location of an event."
@@ -478,31 +510,31 @@ msgstr ""
"Noms et adresses alternatives qui seront utilisées pour associer une adresse "
"avec la localisation en forme libre d'un événement"
-#: agenda_culturel/models.py:459
+#: agenda_culturel/models.py:467
msgid "Places"
msgstr "Lieux"
-#: agenda_culturel/models.py:511
+#: agenda_culturel/models.py:524
msgid "Organisation name"
msgstr "Nom de l'organisme"
-#: agenda_culturel/models.py:515
+#: agenda_culturel/models.py:528
msgid "Website"
msgstr "Site internet"
-#: agenda_culturel/models.py:516
+#: agenda_culturel/models.py:529
msgid "Website of the organisation"
msgstr "Site internet de l'organisme"
-#: agenda_culturel/models.py:524
+#: agenda_culturel/models.py:537
msgid "Description of the organisation."
msgstr "Description de l'organisme"
-#: agenda_culturel/models.py:531
+#: agenda_culturel/models.py:544
msgid "Principal place"
msgstr "Lieu principal"
-#: agenda_culturel/models.py:532
+#: agenda_culturel/models.py:545
msgid ""
"Place mainly associated with this organizer. Mainly used if there is a "
"similarity in the name, to avoid redundant displays."
@@ -510,59 +542,75 @@ msgstr ""
"Lieu principalement associé à cet organisateur. Principalement utilisé s'il "
"y a une similarité de nom, pour éviter les affichages redondants."
-#: agenda_culturel/models.py:539
+#: agenda_culturel/models.py:552
msgid "Organisation"
msgstr "Organisme"
-#: agenda_culturel/models.py:540
+#: agenda_culturel/models.py:553
msgid "Organisations"
msgstr "Organismes"
-#: agenda_culturel/models.py:552 agenda_culturel/models.py:1819
+#: agenda_culturel/models.py:565 agenda_culturel/models.py:1947
msgid "Published"
msgstr "Publié"
-#: agenda_culturel/models.py:553
+#: agenda_culturel/models.py:566
msgid "Draft"
msgstr "Brouillon"
-#: agenda_culturel/models.py:554
+#: agenda_culturel/models.py:567
msgid "Trash"
msgstr "Corbeille"
-#: agenda_culturel/models.py:565
+#: agenda_culturel/models.py:576
+msgid "Author of the event creation"
+msgstr "Auteur de la création de l'événement"
+
+#: agenda_culturel/models.py:584
+msgid "Author of the last importation"
+msgstr "Auteur de la dernière importation"
+
+#: agenda_culturel/models.py:592
+msgid "Author of the last modification"
+msgstr "Auteur de la dernière modification"
+
+#: agenda_culturel/models.py:600
+msgid "Author of the last moderation"
+msgstr "Auteur de la dernière modération"
+
+#: agenda_culturel/models.py:611
msgid "Title"
msgstr "Titre"
-#: agenda_culturel/models.py:569 agenda_culturel/models.py:1916
+#: agenda_culturel/models.py:615 agenda_culturel/models.py:2055
msgid "Status"
msgstr "Status"
-#: agenda_culturel/models.py:581
+#: agenda_culturel/models.py:627
msgid "Start day"
msgstr "Date de début"
-#: agenda_culturel/models.py:584
+#: agenda_culturel/models.py:630
msgid "Start time"
msgstr "Heure de début"
-#: agenda_culturel/models.py:590
+#: agenda_culturel/models.py:636
msgid "End day"
msgstr "Date de fin"
-#: agenda_culturel/models.py:595
+#: agenda_culturel/models.py:641
msgid "End time"
msgstr "Heure de fin"
-#: agenda_culturel/models.py:599
+#: agenda_culturel/models.py:645
msgid "Recurrence"
msgstr "Récurrence"
-#: agenda_culturel/models.py:610
+#: agenda_culturel/models.py:656
msgid "Location (free form)"
msgstr "Localisation (forme libre)"
-#: agenda_culturel/models.py:612
+#: agenda_culturel/models.py:658
msgid ""
"Address of the event in case its not available in the already known places "
"(free form)"
@@ -570,11 +618,11 @@ msgstr ""
"Addresse d'un événement si elle n'est pas déjà présente dans la liste des "
"lieux disponible (forme libre)"
-#: agenda_culturel/models.py:629
+#: agenda_culturel/models.py:675
msgid "Organisers"
msgstr "Organisateurs"
-#: agenda_culturel/models.py:631
+#: agenda_culturel/models.py:677
msgid ""
"list of event organisers. Organizers will only be displayed if one of them "
"does not normally use the venue."
@@ -582,188 +630,207 @@ msgstr ""
"Liste des organisateurs de l'événements. Les organisateurs seront affichés "
"uniquement si au moins un d'entre eux n'utilise pas habituellement le lieu."
-#: agenda_culturel/models.py:644
+#: agenda_culturel/models.py:690
msgid "Illustration (URL)"
msgstr "Illustration (URL)"
-#: agenda_culturel/models.py:645
+#: agenda_culturel/models.py:691
msgid "External URL of the illustration image"
msgstr "URL externe de l'image illustrative"
-#: agenda_culturel/models.py:651
+#: agenda_culturel/models.py:697
msgid "Illustration description"
msgstr "Description de l'illustration"
-#: agenda_culturel/models.py:652
+#: agenda_culturel/models.py:698
msgid "Alternative text used by screen readers for the image"
msgstr "Texte alternatif utiliser par les lecteurs d'écrans pour l'image"
-#: agenda_culturel/models.py:660
+#: agenda_culturel/models.py:706
msgid "Importation source"
msgstr "Source d'importation"
-#: agenda_culturel/models.py:661
+#: agenda_culturel/models.py:707
msgid "Importation source used to detect removed entries."
msgstr "Source d'importation utilisée pour détecter les éléments supprimés/"
-#: agenda_culturel/models.py:667
+#: agenda_culturel/models.py:713
msgid "UUIDs"
msgstr "UUIDs"
-#: agenda_culturel/models.py:668
+#: agenda_culturel/models.py:714
msgid "UUIDs from import to detect duplicated entries."
msgstr "UUIDs utilisés pendant l'import pour détecter les entrées dupliquées"
-#: agenda_culturel/models.py:674
+#: agenda_culturel/models.py:720
msgid "URLs"
msgstr "URLs"
-#: agenda_culturel/models.py:675
+#: agenda_culturel/models.py:721
msgid "List of all the urls where this event can be found."
msgstr "Liste de toutes les urls où l'événement peut être trouvé."
-#: agenda_culturel/models.py:689
+#: agenda_culturel/models.py:735
msgid "Other versions"
msgstr ""
-#: agenda_culturel/models.py:763
+#: agenda_culturel/models.py:813
msgid "Events"
msgstr "Événements"
-#: agenda_culturel/models.py:1685
-msgid "Contact message"
-msgstr "Message de contact"
+#: agenda_culturel/models.py:1597
+msgid ""
+"The duration of the event is a little too long for direct publication. "
+"Moderators can choose to publish it or not."
+msgstr ""
+"La durée de l'événement est un peu trop longue pour qu'il soit publié "
+"directement. Les modérateurs peuvent choisir de le publier ou non."
-#: agenda_culturel/models.py:1686
-msgid "Contact messages"
-msgstr "Messages de contact"
+#: agenda_culturel/models.py:1606
+msgid "Import"
+msgstr "Import"
-#: agenda_culturel/models.py:1689
-msgid "Subject"
-msgstr "Sujet"
+#: agenda_culturel/models.py:1606
+msgid "import process"
+msgstr "processus d'import"
-#: agenda_culturel/models.py:1690
-msgid "The subject of your message"
-msgstr "Sujet de votre message"
-
-#: agenda_culturel/models.py:1696
-#| msgid "Duplicated events"
-msgid "Related event"
-msgstr "Événement associé"
-
-#: agenda_culturel/models.py:1697
-msgid "The message is associated with this event."
-msgstr "Le message est associé à cet événement."
-
-#: agenda_culturel/models.py:1705
-msgid "Your name"
-msgstr "Votre nom"
-
-#: agenda_culturel/models.py:1711
-msgid "Email address"
-msgstr "Adresse email"
-
-#: agenda_culturel/models.py:1712
-msgid "Your email address"
-msgstr "Votre adresse email"
-
-#: agenda_culturel/models.py:1717
+#: agenda_culturel/models.py:1793 agenda_culturel/models.py:1841
msgid "Message"
msgstr "Message"
-#: agenda_culturel/models.py:1717
+#: agenda_culturel/models.py:1794
+msgid "Messages"
+msgstr "Messages"
+
+#: agenda_culturel/models.py:1805
+msgid "Subject"
+msgstr "Sujet"
+
+#: agenda_culturel/models.py:1806
+msgid "The subject of your message"
+msgstr "Sujet de votre message"
+
+#: agenda_culturel/models.py:1812
+msgid "Related event"
+msgstr "Événement associé"
+
+#: agenda_culturel/models.py:1813
+msgid "The message is associated with this event."
+msgstr "Le message est associé à cet événement."
+
+#: agenda_culturel/models.py:1821
+msgid "Author of the message"
+msgstr "Auteur du message"
+
+#: agenda_culturel/models.py:1829
+msgid "Your name"
+msgstr "Votre nom"
+
+#: agenda_culturel/models.py:1835
+msgid "Email address"
+msgstr "Adresse email"
+
+#: agenda_culturel/models.py:1836
+msgid "Your email address"
+msgstr "Votre adresse email"
+
+#: agenda_culturel/models.py:1841
msgid "Your message"
msgstr "Votre message"
-#: agenda_culturel/models.py:1723
+#: agenda_culturel/models.py:1847
msgid "This message is a spam."
msgstr "Ce message est un spam."
-#: agenda_culturel/models.py:1730
+#: agenda_culturel/models.py:1854
msgid "this message has been processed and no longer needs to be handled"
msgstr "Ce message a été traité et ne nécessite plus d'être pris en charge"
-#: agenda_culturel/models.py:1735
+#: agenda_culturel/models.py:1859
msgid "Comments"
msgstr "Commentaires"
-#: agenda_culturel/models.py:1736
+#: agenda_culturel/models.py:1860
msgid "Comments on the message from the moderation team"
msgstr "Commentaires sur ce message par l'équipe de modération"
-#: agenda_culturel/models.py:1748 agenda_culturel/models.py:1896
+#: agenda_culturel/models.py:1875 agenda_culturel/models.py:2035
msgid "Recurrent import"
msgstr "Import récurrent"
-#: agenda_culturel/models.py:1749
+#: agenda_culturel/models.py:1876
msgid "Recurrent imports"
msgstr "Imports récurrents"
-#: agenda_culturel/models.py:1753
+#: agenda_culturel/models.py:1880
msgid "ical"
msgstr "ical"
-#: agenda_culturel/models.py:1754
+#: agenda_culturel/models.py:1881
msgid "ical no busy"
msgstr "ical sans busy"
-#: agenda_culturel/models.py:1755
+#: agenda_culturel/models.py:1882
msgid "ical no VC"
msgstr "ical sans VC"
-#: agenda_culturel/models.py:1756
+#: agenda_culturel/models.py:1883
msgid "lacoope.org"
msgstr "lacoope.org"
-#: agenda_culturel/models.py:1757
+#: agenda_culturel/models.py:1884
msgid "la comédie"
msgstr "la comédie"
-#: agenda_culturel/models.py:1758
+#: agenda_culturel/models.py:1885
msgid "le fotomat"
msgstr "le fotomat"
-#: agenda_culturel/models.py:1759
+#: agenda_culturel/models.py:1886
msgid "la puce à l'oreille"
msgstr "la puce à loreille"
-#: agenda_culturel/models.py:1760
+#: agenda_culturel/models.py:1887
msgid "Plugin wordpress MEC"
msgstr "Plugin wordpress MEC"
-#: agenda_culturel/models.py:1761
+#: agenda_culturel/models.py:1888
msgid "Événements d'une page FB"
msgstr "Événements d'une page FB"
-#: agenda_culturel/models.py:1762
+#: agenda_culturel/models.py:1889
msgid "la cour des 3 coquins"
msgstr "la cour des 3 coquins"
-#: agenda_culturel/models.py:1763
+#: agenda_culturel/models.py:1890
msgid "Arachnée concert"
msgstr "Arachnée concert"
-#: agenda_culturel/models.py:1766
+#: agenda_culturel/models.py:1891
+msgid "Le Rio"
+msgstr "Le Rio"
+
+#: agenda_culturel/models.py:1894
msgid "simple"
msgstr "simple"
-#: agenda_culturel/models.py:1767
+#: agenda_culturel/models.py:1895
msgid "Headless Chromium"
msgstr "chromium sans interface"
-#: agenda_culturel/models.py:1768
+#: agenda_culturel/models.py:1896
msgid "Headless Chromium (pause)"
msgstr "chromium sans interface (pause)"
-#: agenda_culturel/models.py:1773
+#: agenda_culturel/models.py:1901
msgid "daily"
msgstr "chaque jour"
-#: agenda_culturel/models.py:1775
+#: agenda_culturel/models.py:1903
msgid "weekly"
msgstr "chaque semaine"
-#: agenda_culturel/models.py:1780
+#: agenda_culturel/models.py:1908
msgid ""
"Recurrent import name. Be careful to choose a name that is easy to "
"understand, as it will be public and displayed on the sites About page."
@@ -771,143 +838,151 @@ msgstr ""
"Nom de l'import récurrent. Attention à choisir un nom compréhensible, car il "
"sera public, et affiché sur la page à propos du site."
-#: agenda_culturel/models.py:1787
+#: agenda_culturel/models.py:1915
msgid "Processor"
msgstr "Processeur"
-#: agenda_culturel/models.py:1790
+#: agenda_culturel/models.py:1918
msgid "Downloader"
msgstr "Téléchargeur"
-#: agenda_culturel/models.py:1797
+#: agenda_culturel/models.py:1925
msgid "Import recurrence"
msgstr "Récurrence d'import"
-#: agenda_culturel/models.py:1804
+#: agenda_culturel/models.py:1932
msgid "Source"
msgstr "Source"
-#: agenda_culturel/models.py:1805
+#: agenda_culturel/models.py:1933
msgid "URL of the source document"
msgstr "URL du document source"
-#: agenda_culturel/models.py:1809
+#: agenda_culturel/models.py:1937
msgid "Browsable url"
msgstr "URL navigable"
-#: agenda_culturel/models.py:1811
+#: agenda_culturel/models.py:1939
msgid "URL of the corresponding document that will be shown to visitors."
msgstr "URL correspondant au document et qui sera montrée aux visiteurs"
-#: agenda_culturel/models.py:1820
+#: agenda_culturel/models.py:1948
msgid "Status of each imported event (published or draft)"
msgstr "Status de chaque événement importé (publié ou brouillon)"
-#: agenda_culturel/models.py:1825
+#: agenda_culturel/models.py:1953
msgid "Address for each imported event"
msgstr "Adresse de chaque événement importé"
-#: agenda_culturel/models.py:1833
+#: agenda_culturel/models.py:1960
+msgid "Force location"
+msgstr "Focer la localisation"
+
+#: agenda_culturel/models.py:1961
+msgid "force location even if another is detected."
+msgstr "Forcer la localisation même si une autre a été détectée."
+
+#: agenda_culturel/models.py:1967
msgid "Organiser"
msgstr "Organisateur"
-#: agenda_culturel/models.py:1834
+#: agenda_culturel/models.py:1968
msgid "Organiser of each imported event"
msgstr "Organisateur de chaque événement importé"
-#: agenda_culturel/models.py:1844
+#: agenda_culturel/models.py:1978
msgid "Category of each imported event"
msgstr "Catégorie de chaque événement importé"
-#: agenda_culturel/models.py:1852
+#: agenda_culturel/models.py:1986
msgid "Tags for each imported event"
msgstr "Étiquettes de chaque événement importé"
-#: agenda_culturel/models.py:1853
+#: agenda_culturel/models.py:1987
msgid "A list of tags that describe each imported event."
msgstr "Une liste d'étiquettes décrivant chaque événement importé"
-#: agenda_culturel/models.py:1882
+#: agenda_culturel/models.py:2016
msgid "Running"
msgstr "En cours"
-#: agenda_culturel/models.py:1883
+#: agenda_culturel/models.py:2017
msgid "Canceled"
msgstr "Annulé"
-#: agenda_culturel/models.py:1884
+#: agenda_culturel/models.py:2018
msgid "Success"
msgstr "Succès"
-#: agenda_culturel/models.py:1885
+#: agenda_culturel/models.py:2019
msgid "Failed"
msgstr "Erreur"
-#: agenda_culturel/models.py:1888
+#: agenda_culturel/models.py:2022
msgid "Batch importation"
msgstr "Importation par lot"
-#: agenda_culturel/models.py:1889
+#: agenda_culturel/models.py:2023
msgid "Batch importations"
msgstr "Importations par lot"
-#: agenda_culturel/models.py:1897
+#: agenda_culturel/models.py:2036
msgid "Reference to the recurrent import processing"
msgstr "Référence du processus d'import récurrent"
-#: agenda_culturel/models.py:1905
+#: agenda_culturel/models.py:2044
msgid "URL (if not recurrent import)"
msgstr "URL (si pas d'import récurrent)"
-#: agenda_culturel/models.py:1907
+#: agenda_culturel/models.py:2046
msgid "Source URL if no RecurrentImport is associated."
msgstr "URL source si aucun import récurrent n'est associé"
-#: agenda_culturel/models.py:1920
+#: agenda_culturel/models.py:2059
msgid "Error message"
msgstr "Votre message"
-#: agenda_culturel/models.py:1924
+#: agenda_culturel/models.py:2063
msgid "Number of collected events"
msgstr "Nombre d'événements collectés"
-#: agenda_culturel/models.py:1927
+#: agenda_culturel/models.py:2066
msgid "Number of imported events"
msgstr "Nombre d'événements importés"
-#: agenda_culturel/models.py:1930
+#: agenda_culturel/models.py:2069
msgid "Number of updated events"
msgstr "Nombre d'événements mis à jour"
-#: agenda_culturel/models.py:1933
+#: agenda_culturel/models.py:2072
msgid "Number of removed events"
msgstr "Nombre d'événements supprimés"
-#: agenda_culturel/models.py:1941
+#: agenda_culturel/models.py:2080
msgid "Weight"
msgstr "Poids"
-#: agenda_culturel/models.py:1942
+#: agenda_culturel/models.py:2081
msgid "The lower is the weight, the earlier the filter is applied"
msgstr "Plus le poids est léger, plus le filtre sera appliqué tôt"
-#: agenda_culturel/models.py:1949
+#: agenda_culturel/models.py:2088
msgid "Category applied to the event"
msgstr "Catégorie appliquée à l'événement"
-#: agenda_culturel/models.py:1954
+#: agenda_culturel/models.py:2093
msgid "Contained in the title"
msgstr "Contenu dans le titre"
-#: agenda_culturel/models.py:1955
+#: agenda_culturel/models.py:2094
msgid "Text contained in the event title"
msgstr "Texte contenu dans le titre de l'événement"
-#: agenda_culturel/models.py:1961
+#: agenda_culturel/models.py:2100
msgid "Exact title extract"
msgstr "Extrait exact du titre"
-#: agenda_culturel/models.py:1963
+#: agenda_culturel/models.py:2102
msgid ""
"If checked, the extract will be searched for in the title using the exact "
"form (capitals, accents)."
@@ -915,19 +990,19 @@ msgstr ""
"Si coché, l'extrait sera recherché dans le titre en utilisant la forme "
"exacte (majuscules, accents)"
-#: agenda_culturel/models.py:1969
+#: agenda_culturel/models.py:2108
msgid "Contained in the description"
msgstr "Contenu dans la description"
-#: agenda_culturel/models.py:1970
+#: agenda_culturel/models.py:2109
msgid "Text contained in the description"
msgstr "Texte contenu dans la description"
-#: agenda_culturel/models.py:1976
+#: agenda_culturel/models.py:2115
msgid "Exact description extract"
msgstr "Extrait exact de description"
-#: agenda_culturel/models.py:1978
+#: agenda_culturel/models.py:2117
msgid ""
"If checked, the extract will be searched for in the description using the "
"exact form (capitals, accents)."
@@ -935,19 +1010,19 @@ msgstr ""
"Si coché, l'extrait sera recherché dans la description en utilisant la forme "
"exacte (majuscules, accents)"
-#: agenda_culturel/models.py:1984
+#: agenda_culturel/models.py:2123
msgid "Contained in the location"
msgstr "Contenu dans la localisation"
-#: agenda_culturel/models.py:1985
+#: agenda_culturel/models.py:2124
msgid "Text contained in the event location"
msgstr "Texte contenu dans la localisation de l'événement"
-#: agenda_culturel/models.py:1991
+#: agenda_culturel/models.py:2130
msgid "Exact location extract"
msgstr "Extrait exact de localisation"
-#: agenda_culturel/models.py:1993
+#: agenda_culturel/models.py:2132
msgid ""
"If checked, the extract will be searched for in the location using the exact "
"form (capitals, accents)."
@@ -955,43 +1030,43 @@ msgstr ""
"Si coché, l'extrait sera recherché dans la localisation en utilisant la "
"forme exacte (majuscules, accents)"
-#: agenda_culturel/models.py:2001
+#: agenda_culturel/models.py:2140
msgid "Location from place"
msgstr "Localisation depuis le lieu"
-#: agenda_culturel/models.py:2010
+#: agenda_culturel/models.py:2149
msgid "Categorisation rule"
msgstr "Règle de catégorisation"
-#: agenda_culturel/models.py:2011
+#: agenda_culturel/models.py:2150
msgid "Categorisation rules"
msgstr "Règles de catégorisation"
-#: agenda_culturel/settings/base.py:151
+#: agenda_culturel/settings/base.py:153
msgid "French"
msgstr "français"
-#: agenda_culturel/views.py:147
+#: agenda_culturel/views.py:154
msgid "Recurrent import name"
msgstr "Nome de l'import récurrent"
-#: agenda_culturel/views.py:148
+#: agenda_culturel/views.py:155
msgid "Add another"
msgstr "Ajouter un autre"
-#: agenda_culturel/views.py:149
+#: agenda_culturel/views.py:156
msgid "Browse..."
msgstr "Naviguer..."
-#: agenda_culturel/views.py:150
+#: agenda_culturel/views.py:157
msgid "No file selected."
msgstr "Pas de fichier sélectionné."
-#: agenda_culturel/views.py:286
+#: agenda_culturel/views.py:293
msgid "The static content has been successfully updated."
msgstr "Le contenu statique a été modifié avec succès."
-#: agenda_culturel/views.py:294
+#: agenda_culturel/views.py:301
msgid ""
"The event cannot be updated because the import process is not available for "
"the referenced sources."
@@ -999,33 +1074,37 @@ msgstr ""
"La mise à jour de l'événement n'est pas possible car le processus d'import "
"n'est pas disponible pour les sources référencées."
-#: agenda_culturel/views.py:297
+#: agenda_culturel/views.py:304
msgid "The event update has been queued and will be completed shortly."
msgstr ""
"La mise à jour de l'événement a été mise en attente et sera effectuée sous "
"peu."
-#: agenda_culturel/views.py:307
+#: agenda_culturel/views.py:314
msgid "The event has been successfully modified."
msgstr "L'événement a été modifié avec succès."
-#: agenda_culturel/views.py:352
-msgid "The event has been successfully moderated."
-msgstr "L'événement a été modéré avec succès."
+#: agenda_culturel/views.py:365
+msgid "The event {} has been moderated with success."
+msgstr "L'événement {} a été modéré avec succès."
-#: agenda_culturel/views.py:446
+#: agenda_culturel/views.py:480
msgid "The event has been successfully deleted."
msgstr "L'événement a été supprimé avec succès."
-#: agenda_culturel/views.py:478
+#: agenda_culturel/views.py:522
+msgid "Comment"
+msgstr "Commentaire"
+
+#: agenda_culturel/views.py:541
msgid "The status has been successfully modified."
msgstr "Le status a été modifié avec succès."
-#: agenda_culturel/views.py:512
+#: agenda_culturel/views.py:575
msgid "The event was created: {}."
msgstr "L'événement a été créé: {}."
-#: agenda_culturel/views.py:514 agenda_culturel/views.py:538
+#: agenda_culturel/views.py:577
msgid ""
"The event has been submitted and will be published as soon as it has been "
"validated by the moderation team."
@@ -1033,86 +1112,82 @@ msgstr ""
"L'événement a été soumis et sera publié dès qu'il aura été validé par "
"l'équipe de modération."
-#: agenda_culturel/views.py:532
-msgid "The event is saved."
-msgstr "L'événement est enregistré."
-
-#: agenda_culturel/views.py:626 agenda_culturel/views.py:678
+#: agenda_culturel/views.py:671 agenda_culturel/views.py:723
msgid "{} has not been submitted since its already known: {}."
msgstr "{} n'a pas été soumis car il est déjà connu: {}."
-#: agenda_culturel/views.py:631 agenda_culturel/views.py:684
+#: agenda_culturel/views.py:676 agenda_culturel/views.py:729
msgid ""
"{} has not been submitted since its already known and currently into "
"moderation process."
msgstr "{} n'a pas été soumis car il est déjà connu et en cours de modération"
-#: agenda_culturel/views.py:641
+#: agenda_culturel/views.py:686
msgid "Integrating {} url(s) into our import process."
msgstr "Intégration de {} url(s) dans notre processus d'import."
-#: agenda_culturel/views.py:691
+#: agenda_culturel/views.py:736
msgid "Integrating {} into our import process."
msgstr "Intégration de {} dans notre processus d'import."
-#: agenda_culturel/views.py:746
+#: agenda_culturel/views.py:794
msgid "Your message has been sent successfully."
msgstr "Votre message a été envoyé avec succès."
-#: agenda_culturel/views.py:763
+#: agenda_culturel/views.py:822
msgid "Reporting the event {} on {}"
msgstr "Signaler l'événement {} du {}"
-#: agenda_culturel/views.py:771
+#: agenda_culturel/views.py:832
msgid "The contact message has been successfully deleted."
msgstr "Le message de contact a été supprimé avec succès."
-#: agenda_culturel/views.py:785
+#: agenda_culturel/views.py:846
msgid "The contact message properties has been successfully modified."
msgstr "Les propriétés du message de contact ont été modifié avec succès."
-#: agenda_culturel/views.py:922
+#: agenda_culturel/views.py:1015
msgid "Spam has been successfully deleted."
msgstr "Le spam a été supprimé avec succès"
-#: agenda_culturel/views.py:1045
+#: agenda_culturel/views.py:1138
msgid "The import has been run successfully."
msgstr "L'import a été lancé avec succès"
-#: agenda_culturel/views.py:1064
+#: agenda_culturel/views.py:1157
msgid "The import has been canceled."
msgstr "L'import a été annulé"
-#: agenda_culturel/views.py:1142
+#: agenda_culturel/views.py:1235
msgid "The recurrent import has been successfully modified."
msgstr "L'import récurrent a été modifié avec succès."
-#: agenda_culturel/views.py:1151
+#: agenda_culturel/views.py:1244
msgid "The recurrent import has been successfully deleted."
msgstr "L'import récurrent a été supprimé avec succès"
-#: agenda_culturel/views.py:1191
+#: agenda_culturel/views.py:1284
msgid "The import has been launched."
msgstr "L'import a été lancé"
-#: agenda_culturel/views.py:1213
+#: agenda_culturel/views.py:1306
msgid "Imports has been launched."
msgstr "Les imports ont été lancés"
-#: agenda_culturel/views.py:1275
+#: agenda_culturel/views.py:1368
msgid "Update successfully completed."
msgstr "Mise à jour réalisée avec succès."
-#: agenda_culturel/views.py:1336
+#: agenda_culturel/views.py:1429
msgid "Creation of a merged event has been successfully completed."
msgstr "Création d'un événement fusionné réalisée avec succès."
-#: agenda_culturel/views.py:1372
+#: agenda_culturel/views.py:1465
msgid "Events have been marked as unduplicated."
msgstr "Les événements ont été marqués comme non dupliqués."
-#: agenda_culturel/views.py:1386 agenda_culturel/views.py:1395
-#: agenda_culturel/views.py:1413
+#: agenda_culturel/views.py:1479 agenda_culturel/views.py:1488
+#: agenda_culturel/views.py:1506
msgid ""
"The selected item is no longer included in the list of duplicates. Someone "
"else has probably modified the list in the meantime."
@@ -1120,23 +1195,23 @@ msgstr ""
"L'élément sélectionné ne fait plus partie de la liste des dupliqués. Une "
"autre personne a probablement modifié la liste entre temps."
-#: agenda_culturel/views.py:1389
+#: agenda_culturel/views.py:1482
msgid "The selected event has been set as representative"
msgstr "L'événement sélectionné a été défini comme representatif."
-#: agenda_culturel/views.py:1404
+#: agenda_culturel/views.py:1497
msgid "The event has been withdrawn from the group and made independent."
msgstr "L'événement a été retiré du groupe et rendu indépendant."
-#: agenda_culturel/views.py:1448
+#: agenda_culturel/views.py:1541
msgid "Cleaning up duplicates: {} item(s) fixed."
msgstr "Nettoyage des dupliqués: {} élément(s) corrigé(s)."
-#: agenda_culturel/views.py:1497
+#: agenda_culturel/views.py:1590
msgid "The event was successfully duplicated."
msgstr "L'événement a été marqué dupliqué avec succès."
-#: agenda_culturel/views.py:1505
+#: agenda_culturel/views.py:1598
msgid ""
"The event has been successfully flagged as a duplicate. The moderation team "
"will deal with your suggestion shortly."
@@ -1144,32 +1219,32 @@ msgstr ""
"L'événement a été signalé comme dupliqué avec succès. Votre suggestion sera "
"prochainement prise en charge par l'équipe de modération."
-#: agenda_culturel/views.py:1558
+#: agenda_culturel/views.py:1651
msgid "The categorisation rule has been successfully modified."
msgstr "La règle de catégorisation a été modifiée avec succès."
-#: agenda_culturel/views.py:1567
+#: agenda_culturel/views.py:1660
msgid "The categorisation rule has been successfully deleted."
msgstr "La règle de catégorisation a été supprimée avec succès"
-#: agenda_culturel/views.py:1589
+#: agenda_culturel/views.py:1682
msgid "The rules were successfully applied and 1 event was categorised."
msgstr ""
"Les règles ont été appliquées avec succès et 1 événement a été catégorisé"
-#: agenda_culturel/views.py:1596
+#: agenda_culturel/views.py:1689
msgid "The rules were successfully applied and {} events were categorised."
msgstr ""
"Les règles ont été appliquées avec succès et {} événements ont été "
"catégorisés"
-#: agenda_culturel/views.py:1603 agenda_culturel/views.py:1656
+#: agenda_culturel/views.py:1696 agenda_culturel/views.py:1749
msgid "The rules were successfully applied and no events were categorised."
msgstr ""
"Les règles ont été appliquées avec succès et aucun événement n'a été "
"catégorisé"
-#: agenda_culturel/views.py:1642
+#: agenda_culturel/views.py:1735
msgid ""
"The rules were successfully applied and 1 event with default category was "
"categorised."
@@ -1177,7 +1252,7 @@ msgstr ""
"Les règles ont été appliquées avec succès et 1 événement avec catégorie par "
"défaut a été catégorisé"
-#: agenda_culturel/views.py:1649
+#: agenda_culturel/views.py:1742
msgid ""
"The rules were successfully applied and {} events with default category were "
"categorised."
@@ -1185,58 +1260,58 @@ msgstr ""
"Les règles ont été appliquées avec succès et {} événements avec catégorie "
"par défaut ont été catégorisés"
-#: agenda_culturel/views.py:1741 agenda_culturel/views.py:1803
-#: agenda_culturel/views.py:1841
+#: agenda_culturel/views.py:1834 agenda_culturel/views.py:1896
+#: agenda_culturel/views.py:1934
msgid "{} events have been updated."
msgstr "{} événements ont été mis à jour."
-#: agenda_culturel/views.py:1744 agenda_culturel/views.py:1805
-#: agenda_culturel/views.py:1844
+#: agenda_culturel/views.py:1837 agenda_culturel/views.py:1898
+#: agenda_culturel/views.py:1937
msgid "1 event has been updated."
msgstr "1 événement a été mis à jour"
-#: agenda_culturel/views.py:1746 agenda_culturel/views.py:1807
-#: agenda_culturel/views.py:1846
+#: agenda_culturel/views.py:1839 agenda_culturel/views.py:1900
+#: agenda_culturel/views.py:1939
msgid "No events have been modified."
msgstr "Aucun événement n'a été modifié."
-#: agenda_culturel/views.py:1755
+#: agenda_culturel/views.py:1848
msgid "The place has been successfully updated."
msgstr "Le lieu a été modifié avec succès."
-#: agenda_culturel/views.py:1764
+#: agenda_culturel/views.py:1857
msgid "The place has been successfully created."
msgstr "Le lieu a été créé avec succès."
-#: agenda_culturel/views.py:1829
+#: agenda_culturel/views.py:1922
msgid "The selected place has been assigned to the event."
msgstr "Le lieu sélectionné a été assigné à l'événement."
-#: agenda_culturel/views.py:1833
+#: agenda_culturel/views.py:1926
msgid "A new alias has been added to the selected place."
msgstr "Un nouvel alias a été créé pour le lieu sélectionné."
-#: agenda_culturel/views.py:1926
+#: agenda_culturel/views.py:2028
msgid "The organisation has been successfully updated."
msgstr "L'organisme a été modifié avec succès."
-#: agenda_culturel/views.py:1935
+#: agenda_culturel/views.py:2037
msgid "The organisation has been successfully created."
msgstr "L'organisme a été créé avec succès."
-#: agenda_culturel/views.py:1952
+#: agenda_culturel/views.py:2054
msgid "The tag has been successfully updated."
msgstr "L'étiquette a été modifiée avec succès."
-#: agenda_culturel/views.py:1959
+#: agenda_culturel/views.py:2061
msgid "The tag has been successfully created."
msgstr "L'étiquette a été créée avec succès."
-#: agenda_culturel/views.py:2023
+#: agenda_culturel/views.py:2125
msgid "You have not modified the tag name."
msgstr "Vous n'avez pas modifié le nom de l'étiquette."
-#: agenda_culturel/views.py:2033
+#: agenda_culturel/views.py:2135
msgid ""
"This tag {} is already in use, and is described by different information "
"from the current tag. You can force renaming by checking the corresponding "
@@ -1249,7 +1324,7 @@ msgstr ""
"sera supprimée, et tous les événements associés à l'étiquette {} seront "
"associés à l'étiquette {}."
-#: agenda_culturel/views.py:2040
+#: agenda_culturel/views.py:2142
msgid ""
"This tag {} is already in use. You can force renaming by checking the "
"corresponding option."
@@ -1257,10 +1332,14 @@ msgstr ""
"Cette étiquette {} est déjà utilisée. Vous pouvez forcer le renommage en "
"cochant l'option correspondante."
-#: agenda_culturel/views.py:2074
+#: agenda_culturel/views.py:2176
msgid "The tag {} has been successfully renamed to {}."
msgstr "L'étiquette {} a été renommée avec succès en {}."
-#: agenda_culturel/views.py:2112
+#: agenda_culturel/views.py:2214
msgid "The tag {} has been successfully deleted."
msgstr "L'événement {} a été supprimé avec succès."
+
+#: agenda_culturel/views.py:2235
+msgid "Cache successfully cleared."
+msgstr "Le cache a été vidé avec succès."
diff --git a/src/agenda_culturel/migrations/0081_auto_20241010_2235.py b/src/agenda_culturel/migrations/0081_auto_20241010_2235.py
index 1f6e475..fcc4fd3 100644
--- a/src/agenda_culturel/migrations/0081_auto_20241010_2235.py
+++ b/src/agenda_culturel/migrations/0081_auto_20241010_2235.py
@@ -1,11 +1,11 @@
# Generated by Django 4.2.9 on 2024-10-10 20:35
from django.db import migrations
-from agenda_culturel.models import Place
from django.contrib.gis.geos import Point
def change_coord_format(apps, schema_editor):
- places = Place.objects.all()
+ Place = apps.get_model("agenda_culturel", "Place")
+ places = Place.objects.values("location", "location_pt").all()
for p in places:
l = p.location.split(',')
@@ -13,14 +13,15 @@ def change_coord_format(apps, schema_editor):
p.location_pt = Point(float(l[1]), float(l[0]))
else:
p.location_pt = Point(3.08333, 45.783329)
- p.save()
+ p.save(update_fields=["location_pt"])
def reverse_coord_format(apps, schema_editor):
- places = Place.objects.all()
+ Place = apps.get_model("agenda_culturel", "Place")
+ places = Place.objects.values("location", "location_pt").all()
for p in places:
p.location = ','.join([p.location_pt[1], p.location_pt[0]])
- p.save()
+ p.save(update_fields=["location"])
diff --git a/src/agenda_culturel/migrations/0122_alter_recurrentimport_processor.py b/src/agenda_culturel/migrations/0122_alter_recurrentimport_processor.py
new file mode 100644
index 0000000..0758649
--- /dev/null
+++ b/src/agenda_culturel/migrations/0122_alter_recurrentimport_processor.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.9 on 2024-11-29 13:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0121_contactmessage_related_event'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='recurrentimport',
+ name='processor',
+ field=models.CharField(choices=[('ical', 'ical'), ('icalnobusy', 'ical no busy'), ('icalnovc', 'ical no VC'), ('lacoope', 'lacoope.org'), ('lacomedie', 'la comédie'), ('lefotomat', 'le fotomat'), ('lapucealoreille', "la puce à l'oreille"), ('Plugin wordpress MEC', 'Plugin wordpress MEC'), ('Facebook events', "Événements d'une page FB"), ('cour3coquins', 'la cour des 3 coquins'), ('arachnee', 'Arachnée concert'), ('rio', 'Le Rio')], default='ical', max_length=20, verbose_name='Processor'),
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0123_event_created_by_user_event_imported_by_user_and_more.py b/src/agenda_culturel/migrations/0123_event_created_by_user_event_imported_by_user_and_more.py
new file mode 100644
index 0000000..a8359ce
--- /dev/null
+++ b/src/agenda_culturel/migrations/0123_event_created_by_user_event_imported_by_user_and_more.py
@@ -0,0 +1,36 @@
+# Generated by Django 4.2.9 on 2024-11-29 18:18
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('agenda_culturel', '0122_alter_recurrentimport_processor'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='event',
+ name='created_by_user',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='Author of the event creation'),
+ ),
+ migrations.AddField(
+ model_name='event',
+ name='imported_by_user',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='imported_events', to=settings.AUTH_USER_MODEL, verbose_name='Author of the last importation'),
+ ),
+ migrations.AddField(
+ model_name='event',
+ name='moderated_by_user',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='moderated_events', to=settings.AUTH_USER_MODEL, verbose_name='Author of the last moderation'),
+ ),
+ migrations.AddField(
+ model_name='event',
+ name='modified_by_user',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modified_events', to=settings.AUTH_USER_MODEL, verbose_name='Author of the last modification'),
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0124_place_postcode.py b/src/agenda_culturel/migrations/0124_place_postcode.py
new file mode 100644
index 0000000..9581868
--- /dev/null
+++ b/src/agenda_culturel/migrations/0124_place_postcode.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.9 on 2024-12-06 21:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0123_event_created_by_user_event_imported_by_user_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='place',
+ name='postcode',
+ field=models.CharField(blank=True, help_text='The post code is not displayed, but makes it easier to find an address when you enter it.', null=True, verbose_name='Postcode'),
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0125_rename_contactmessage_message_alter_message_options.py b/src/agenda_culturel/migrations/0125_rename_contactmessage_message_alter_message_options.py
new file mode 100644
index 0000000..958fb15
--- /dev/null
+++ b/src/agenda_culturel/migrations/0125_rename_contactmessage_message_alter_message_options.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.9 on 2024-12-11 11:33
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0124_place_postcode'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='ContactMessage',
+ new_name='Message',
+ ),
+ migrations.AlterModelOptions(
+ name='message',
+ options={'verbose_name': 'Message', 'verbose_name_plural': 'Messages'},
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0126_message_user.py b/src/agenda_culturel/migrations/0126_message_user.py
new file mode 100644
index 0000000..42caa87
--- /dev/null
+++ b/src/agenda_culturel/migrations/0126_message_user.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.9 on 2024-12-11 11:56
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('agenda_culturel', '0125_rename_contactmessage_message_alter_message_options'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='message',
+ name='user',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to=settings.AUTH_USER_MODEL, verbose_name='Author of the message'),
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0127_event_agenda_cult_end_day_4660a5_idx_and_more.py b/src/agenda_culturel/migrations/0127_event_agenda_cult_end_day_4660a5_idx_and_more.py
new file mode 100644
index 0000000..d241af1
--- /dev/null
+++ b/src/agenda_culturel/migrations/0127_event_agenda_cult_end_day_4660a5_idx_and_more.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.9 on 2024-12-11 19:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0126_message_user'),
+ ]
+
+ operations = [
+ migrations.AddIndex(
+ model_name='event',
+ index=models.Index(fields=['end_day', 'end_time'], name='agenda_cult_end_day_4660a5_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='event',
+ index=models.Index(fields=['status'], name='agenda_cult_status_893243_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='event',
+ index=models.Index(fields=['recurrence_dtstart', 'recurrence_dtend'], name='agenda_cult_recurre_a8911c_idx'),
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0128_event_datetimes_title.py b/src/agenda_culturel/migrations/0128_event_datetimes_title.py
new file mode 100644
index 0000000..2b0ca51
--- /dev/null
+++ b/src/agenda_culturel/migrations/0128_event_datetimes_title.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.9 on 2024-12-11 19:12
+
+from django.db import migrations, models
+import django.db.models.functions.text
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0127_event_agenda_cult_end_day_4660a5_idx_and_more'),
+ ]
+
+ operations = [
+ migrations.AddIndex(
+ model_name='event',
+ index=models.Index(models.F('start_time'), models.F('start_day'), models.F('end_day'), models.F('end_time'), django.db.models.functions.text.Lower('title'), name='datetimes title'),
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0129_batchimportation_agenda_cult_created_a23990_idx_and_more.py b/src/agenda_culturel/migrations/0129_batchimportation_agenda_cult_created_a23990_idx_and_more.py
new file mode 100644
index 0000000..d88f1c2
--- /dev/null
+++ b/src/agenda_culturel/migrations/0129_batchimportation_agenda_cult_created_a23990_idx_and_more.py
@@ -0,0 +1,57 @@
+# Generated by Django 4.2.9 on 2024-12-11 19:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0128_event_datetimes_title'),
+ ]
+
+ operations = [
+ migrations.AddIndex(
+ model_name='batchimportation',
+ index=models.Index(fields=['created_date'], name='agenda_cult_created_a23990_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='batchimportation',
+ index=models.Index(fields=['status'], name='agenda_cult_status_54b205_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='batchimportation',
+ index=models.Index(fields=['created_date', 'recurrentImport'], name='agenda_cult_created_0296e4_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='duplicatedevents',
+ index=models.Index(fields=['representative'], name='agenda_cult_represe_9a4fa2_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='message',
+ index=models.Index(fields=['related_event'], name='agenda_cult_related_79de3c_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='message',
+ index=models.Index(fields=['user'], name='agenda_cult_user_id_42dc88_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='message',
+ index=models.Index(fields=['date'], name='agenda_cult_date_049c71_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='message',
+ index=models.Index(fields=['spam', 'closed'], name='agenda_cult_spam_22f9b3_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='place',
+ index=models.Index(fields=['name'], name='agenda_cult_name_222846_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='place',
+ index=models.Index(fields=['city'], name='agenda_cult_city_156dc7_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='place',
+ index=models.Index(fields=['location'], name='agenda_cult_locatio_6f3c05_idx'),
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0130_recurrentimport_forcelocation.py b/src/agenda_culturel/migrations/0130_recurrentimport_forcelocation.py
new file mode 100644
index 0000000..9befee0
--- /dev/null
+++ b/src/agenda_culturel/migrations/0130_recurrentimport_forcelocation.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.9 on 2024-12-22 15:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0129_batchimportation_agenda_cult_created_a23990_idx_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='recurrentimport',
+ name='forceLocation',
+ field=models.BooleanField(default=False, help_text='force location even if another is detected.', verbose_name='Force location'),
+ ),
+ ]
diff --git a/src/agenda_culturel/models.py b/src/agenda_culturel/models.py
index d7b3909..c6071c5 100644
--- a/src/agenda_culturel/models.py
+++ b/src/agenda_culturel/models.py
@@ -10,7 +10,11 @@ from colorfield.fields import ColorField
from django_ckeditor_5.fields import CKEditor5Field
from urllib.parse import urlparse
from django.core.cache import cache
+from django.core.cache.utils import make_template_fragment_key
+from django.contrib.auth.models import User, AnonymousUser
import emoji
+from django.core.files.storage import default_storage
+import uuid
import hashlib
import urllib.request
@@ -20,6 +24,7 @@ from django.utils import timezone
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q, Count, F, Subquery, OuterRef, Func
from django.db.models.functions import Lower
+from django.contrib.postgres.lookups import Unaccent
import recurrence.fields
import recurrence
import copy
@@ -284,6 +289,10 @@ class DuplicatedEvents(models.Model):
class Meta:
verbose_name = _("Duplicated events")
verbose_name_plural = _("Duplicated events")
+ indexes = [
+ models.Index(fields=['representative']),
+ ]
+
def __init__(self, *args, **kwargs):
self.events = None
@@ -434,8 +443,9 @@ class Place(models.Model):
blank=True,
null=True,
)
+ postcode = models.CharField(verbose_name=_("Postcode"), help_text=_("The post code is not displayed, but makes it easier to find an address when you enter it."), blank=True, null=True)
city = models.CharField(verbose_name=_("City"), help_text=_("City name"))
- location = LocationField(based_fields=["name", "address", "city"], zoom=12, default=Point(3.08333, 45.783329))
+ location = LocationField(based_fields=["name", "address", "postcode", "city"], zoom=12, default=Point(3.08333, 45.783329))
description = CKEditor5Field(
verbose_name=_("Description"),
@@ -458,6 +468,11 @@ class Place(models.Model):
verbose_name = _("Place")
verbose_name_plural = _("Places")
ordering = ["name"]
+ indexes = [
+ models.Index(fields=['name']),
+ models.Index(fields=['city']),
+ models.Index(fields=['location']),
+ ]
def __str__(self):
if self.address:
@@ -543,7 +558,7 @@ class Organisation(models.Model):
return self.name
def get_absolute_url(self):
- return reverse("view_organisation", kwargs={'pk': self.pk})
+ return reverse("view_organisation", kwargs={'pk': self.pk, "extra": self.name})
@@ -558,6 +573,39 @@ class Event(models.Model):
modified_date = models.DateTimeField(blank=True, null=True)
moderated_date = models.DateTimeField(blank=True, null=True)
+ created_by_user = models.ForeignKey(
+ User,
+ verbose_name=_("Author of the event creation"),
+ null=True,
+ default=None,
+ on_delete=models.SET_DEFAULT,
+ related_name="created_events"
+ )
+ imported_by_user = models.ForeignKey(
+ User,
+ verbose_name=_("Author of the last importation"),
+ null=True,
+ default=None,
+ on_delete=models.SET_DEFAULT,
+ related_name="imported_events"
+ )
+ modified_by_user = models.ForeignKey(
+ User,
+ verbose_name=_("Author of the last modification"),
+ null=True,
+ default=None,
+ on_delete=models.SET_DEFAULT,
+ related_name="modified_events"
+ )
+ moderated_by_user = models.ForeignKey(
+ User,
+ verbose_name=_("Author of the last moderation"),
+ null=True,
+ default=None,
+ on_delete=models.SET_DEFAULT,
+ related_name="moderated_events"
+ )
+
recurrence_dtstart = models.DateTimeField(editable=False, blank=True, null=True)
recurrence_dtend = models.DateTimeField(editable=False, blank=True, null=True)
@@ -692,6 +740,10 @@ class Event(models.Model):
blank=True,
)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.processing_user = None
+
def get_consolidated_end_day(self, intuitive=True):
if intuitive:
end_day = self.get_consolidated_end_day(False)
@@ -709,15 +761,6 @@ 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, remove_same_dup=True):
- first = self.start_day
- last = self.get_consolidated_end_day()
- ignore_dup = None
- if remove_same_dup:
- ignore_dup = self.other_versions
- calendar = CalendarList(first, last, exact=True, ignore_dup=ignore_dup)
- return [(len(d.events), d.date) for dstr, d in calendar.get_calendar_days().items()]
-
def is_single_day(self, intuitive=True):
return self.start_day == self.get_consolidated_end_day(intuitive)
@@ -727,6 +770,15 @@ class Event(models.Model):
end_date = parse_date(end_date)
return parse_date(self.start_day) + timedelta(days=min_days) < end_date
+ def set_message(self, msg):
+ self._message = msg
+
+ def get_message(self):
+ return self._message
+
+ def has_message(self):
+ return hasattr(self, '_message')
+
def contains_date(self, d, intuitive=True):
return d >= self.start_day and d <= self.get_consolidated_end_day(intuitive)
@@ -764,9 +816,36 @@ class Event(models.Model):
permissions = [("set_duplicated_event", "Can set an event as duplicated")]
indexes = [
models.Index(fields=["start_day", "start_time"]),
- models.Index("start_time", Lower("title"), name="start_time title")
+ models.Index(fields=["end_day", "end_time"]),
+ models.Index(fields=["status"]),
+ models.Index(fields=["recurrence_dtstart", "recurrence_dtend"]),
+ models.Index("start_time", Lower("title"), name="start_time title"),
+ models.Index("start_time", "start_day", "end_day", "end_time", Lower("title"), name="datetimes title")
]
+ def chronology(self):
+ c = []
+ if self.modified_date:
+ c.append({ "timestamp": self.modified_date, "data": "modified_date", "user": self.modified_by_user, "is_date": True })
+ if self.moderated_date:
+ c.append({ "timestamp": self.moderated_date, "data": "moderated_date", "user" : self.moderated_by_user, "is_date": True})
+ if self.imported_date:
+ c.append({ "timestamp": self.imported_date, "data": "imported_date", "user": self.imported_by_user, "is_date": True })
+ if self.created_date:
+ c.append({ "timestamp": self.created_date + timedelta(milliseconds=-1), "data": "created_date", "user": self.created_by_user, "is_date": True})
+
+ c += [{ "timestamp": m.date, "data": m, "user": m.user, "is_date": False} for m in self.message_set.filter(spam=False)]
+
+ if self.other_versions:
+ for o in self.other_versions.get_duplicated():
+ if o != self:
+ c += [{ "timestamp": m.date, "data": m, "user": m.user, "is_date": False} for m in o.message_set.filter(spam=False)]
+
+
+ c.sort(key=lambda x: x["timestamp"])
+
+ return c
+
def sorted_tags(self):
if self.tags is None:
return []
@@ -858,19 +937,28 @@ class Event(models.Model):
def is_representative(self):
return self.other_versions is None or self.other_versions.representative == self
+ def download_missing_image(self):
+ if self.local_image and not default_storage.exists(self.local_image.name):
+ logger.warning("on dl")
+ self.download_image()
+ self.save(update_fields=["local_image"])
+
def download_image(self):
# first download file
a = urlparse(self.image)
basename = os.path.basename(a.path)
+ ext = basename.split('.')[-1]
+ filename = "%s.%s" % (uuid.uuid4(), ext)
+
try:
tmpfile, _ = urllib.request.urlretrieve(self.image)
except:
return None
# if the download is ok, then create the corresponding file object
- self.local_image = File(name=basename, file=open(tmpfile, "rb"))
+ self.local_image = File(name=filename, file=open(tmpfile, "rb"))
def add_pending_organisers(self, organisers):
self.pending_organisers = organisers
@@ -896,6 +984,12 @@ class Event(models.Model):
def set_no_modification_date_changed(self):
self.no_modification_date_changed = True
+ def set_processing_user(self, user):
+ if user is None or user.is_anonymous:
+ self.processing_user = None
+ else:
+ self.processing_user = user
+
def set_in_moderation_process(self):
self.in_moderation_process = True
@@ -906,12 +1000,16 @@ class Event(models.Model):
now = timezone.now()
if not self.id:
self.created_date = now
+ self.created_by_user = self.processing_user
if self.is_in_importation_process():
self.imported_date = now
+ self.imported_by_user = self.processing_user
if self.modified_date is None or not self.is_no_modification_date_changed():
self.modified_date = now
+ self.modified_by_user = self.processing_user
if self.is_in_moderation_process():
self.moderated_date = now
+ self.moderated_by_user = self.processing_user
def get_recurrence_at_date(self, year, month, day):
dtstart = timezone.make_aware(
@@ -923,10 +1021,13 @@ class Event(models.Model):
else:
return recurrences[0]
- def get_image_url(self):
+ def get_image_url(self, request=None):
if self.local_image and hasattr(self.local_image, "url"):
try:
- return self.local_image.url
+ if request:
+ return request.build_absolute_uri(self.local_image.url)
+ else:
+ return self.local_image.url
except:
pass
if self.image:
@@ -1024,7 +1125,7 @@ class Event(models.Model):
self.update_recurrence_dtstartend()
# if the image is defined but not locally downloaded
- if self.image and not self.local_image:
+ if self.image and (not self.local_image or not default_storage.exists(self.local_image.name)):
self.download_image()
# remove "/" from tags
@@ -1076,6 +1177,11 @@ class Event(models.Model):
# first save the current object
super().save(*args, **kwargs)
+ # clear cache
+ for is_auth in [False, True]:
+ key = make_template_fragment_key("event_body", [is_auth, self])
+ cache.delete(key)
+
# then if its a clone, update the representative
if clone:
self.other_versions.representative = self
@@ -1322,9 +1428,10 @@ class Event(models.Model):
# otherwise merge existing groups
group = DuplicatedEvents.merge_groups(groups)
+ group.save()
+
if force_non_fixed:
group.representative = None
- group.save()
# set the possibly duplicated group for the current object
self.other_versions = group
@@ -1345,6 +1452,8 @@ class Event(models.Model):
"category",
"tags",
]
+ if not no_m2m:
+ result += ["organisers"]
result += [
"title",
@@ -1356,8 +1465,6 @@ class Event(models.Model):
"description",
"image",
]
- if not no_m2m:
- result += ["organisers"]
if all and local_img:
result += ["local_image"]
if all and exact_location:
@@ -1409,7 +1516,11 @@ class Event(models.Model):
self.import_sources.append(source)
# Limitation: the given events should not be considered similar one to another...
- def import_events(events, remove_missing_from_source=None):
+ def import_events(events, remove_missing_from_source=None, user_id=None):
+
+ user = None
+ if user_id:
+ user = User.objects.filter(pk=user_id).first()
to_import = []
to_update = []
@@ -1436,6 +1547,7 @@ class Event(models.Model):
# imported events should be updated
event.set_in_importation_process()
+ event.set_processing_user(user)
event.prepare_save()
# check if the event has already be imported (using uuid)
@@ -1462,9 +1574,14 @@ class Event(models.Model):
same_imported.other_versions.representative = None
same_imported.other_versions.save()
# we only update local information if it's a pure import and has no moderated_date
+ new_image = same_imported.image != event.image
same_imported.update(event, pure and same_imported.moderated_date is None)
same_imported.set_in_importation_process()
same_imported.prepare_save()
+ # fix missing or updated files
+ if same_imported.local_image and (not default_storage.exists(same_imported.local_image.name) or new_image):
+ same_imported.download_image()
+ same_imported.save(update_fields=["local_image"])
to_update.append(same_imported)
else:
# otherwise, the new event possibly a duplication of the remaining others.
@@ -1492,6 +1609,7 @@ class Event(models.Model):
for e in to_import:
if e.is_event_long_duration():
e.status = Event.STATUS.DRAFT
+ e.set_message(_("The duration of the event is a little too long for direct publication. Moderators can choose to publish it or not."))
# then import all the new events
imported = Event.objects.bulk_create(to_import)
@@ -1499,6 +1617,9 @@ class Event(models.Model):
for i, ti in zip(imported, to_import):
if ti.has_pending_organisers() and ti.pending_organisers is not None:
i.organisers.set(ti.pending_organisers)
+ if ti.has_message():
+ msg = Message(subject=_('Import'), related_event=i, name=_('import process'), message=ti.get_message())
+ msg.save()
nb_updated = Event.objects.bulk_update(
to_update,
@@ -1572,13 +1693,12 @@ class Event(models.Model):
def get_concurrent_events(self, remove_same_dup=True):
day = self.current_date if hasattr(self, "current_date") else self.start_day
- day_events = CalendarDay(self.start_day).get_events()
+ day_events = CalendarDay(day, qs = Event.objects.filter(status=Event.STATUS.PUBLISHED)).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
and (e.other_versions is None or e.other_versions != self.other_versions)
]
@@ -1588,10 +1708,10 @@ class Event(models.Model):
return (dtstart <= e_dtstart <= dtend) or (e_dtstart <= dtstart <= e_dtend)
- def export_to_ics(events):
+ def export_to_ics(events, request):
cal = icalCal()
# Some properties are required to be compliant
- cal.add("prodid", "-//My calendar product//example.com//")
+ cal.add("prodid", "-//Pommes de lune//pommesdelune.fr//")
cal.add("version", "2.0")
for event in events:
@@ -1642,9 +1762,12 @@ class Event(models.Model):
eventIcal.add("summary", event.title)
eventIcal.add("name", event.title)
url = ("\n" + event.reference_urls[0]) if event.reference_urls and len(event.reference_urls) > 0 else ""
+ description = event.description if event.description else ""
eventIcal.add(
- "description", event.description + url
+ "description", description + url
)
+ if not event.local_image is None and event.local_image != "":
+ eventIcal.add('image', request.build_absolute_uri(event.local_image), parameters={'VALUE': 'URI'})
eventIcal.add("location", event.exact_location or event.location)
cal.add_component(eventIcal)
@@ -1680,10 +1803,18 @@ class Event(models.Model):
return [Event.get_count_modification(w) for w in when_list]
-class ContactMessage(models.Model):
+class Message(models.Model):
class Meta:
- verbose_name = _("Contact message")
- verbose_name_plural = _("Contact messages")
+ verbose_name = _("Message")
+ verbose_name_plural = _("Messages")
+ indexes = [
+ models.Index(fields=['related_event']),
+ models.Index(fields=['user']),
+ models.Index(fields=['date']),
+ models.Index(fields=['spam', 'closed']),
+ ]
+
+
subject = models.CharField(
verbose_name=_("Subject"),
@@ -1700,6 +1831,14 @@ class ContactMessage(models.Model):
on_delete=models.SET_DEFAULT,
)
+ user = models.ForeignKey(
+ User,
+ verbose_name=_("Author of the message"),
+ null=True,
+ default=None,
+ on_delete=models.SET_DEFAULT,
+ )
+
name = models.CharField(
verbose_name=_("Name"),
help_text=_("Your name"),
@@ -1739,11 +1878,11 @@ class ContactMessage(models.Model):
null=True,
)
- def nb_open_contactmessages():
- return ContactMessage.objects.filter(closed=False).count()
+ def nb_open_messages():
+ return Message.objects.filter(Q(closed=False)&Q(spam=False)).count()
def get_absolute_url(self):
- return reverse("contactmessage", kwargs={"pk": self.pk})
+ return reverse("message", kwargs={"pk": self.pk})
class RecurrentImport(models.Model):
@@ -1764,6 +1903,7 @@ class RecurrentImport(models.Model):
FBEVENTS = "Facebook events", _("Événements d'une page FB")
C3C = "cour3coquins", _("la cour des 3 coquins")
ARACHNEE = "arachnee", _("Arachnée concert")
+ LERIO = "rio", _('Le Rio')
class DOWNLOADER(models.TextChoices):
SIMPLE = "simple", _("simple")
@@ -1831,6 +1971,12 @@ class RecurrentImport(models.Model):
blank=True,
)
+ forceLocation = models.BooleanField(
+ verbose_name=_("Force location"),
+ help_text=_("force location even if another is detected."),
+ default=False
+ )
+
defaultOrganiser = models.ForeignKey(
Organisation,
verbose_name=_("Organiser"),
@@ -1891,6 +2037,11 @@ class BatchImportation(models.Model):
verbose_name = _("Batch importation")
verbose_name_plural = _("Batch importations")
permissions = [("run_batchimportation", "Can run a batch importation")]
+ indexes = [
+ models.Index(fields=['created_date']),
+ models.Index(fields=['status']),
+ models.Index(fields=['created_date', 'recurrentImport']),
+ ]
created_date = models.DateTimeField(auto_now_add=True)
diff --git a/src/agenda_culturel/settings/base.py b/src/agenda_culturel/settings/base.py
index 9df688b..8fdd6d4 100644
--- a/src/agenda_culturel/settings/base.py
+++ b/src/agenda_culturel/settings/base.py
@@ -56,9 +56,10 @@ INSTALLED_APPS = [
"robots",
"debug_toolbar",
"cache_cleaner",
+ "honeypot",
]
-SITE_ID = 1
+HONEYPOT_FIELD_NAME = "alias_name"
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
@@ -72,6 +73,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
+ 'django.contrib.sites.middleware.CurrentSiteMiddleware',
# "django.middleware.cache.UpdateCacheMiddleware",
# "django.middleware.common.CommonMiddleware",
# "django.middleware.cache.FetchFromCacheMiddleware",
@@ -145,7 +147,7 @@ TIME_ZONE = "Europe/Paris"
USE_I18N = True
-USE_TZ = True
+USE_TZ = False
LANGUAGES = (
("fr", _("French")),
diff --git a/src/agenda_culturel/sitemaps.py b/src/agenda_culturel/sitemaps.py
new file mode 100644
index 0000000..bc74502
--- /dev/null
+++ b/src/agenda_culturel/sitemaps.py
@@ -0,0 +1,13 @@
+from django.contrib import sitemaps
+from django.urls import reverse
+
+
+class StaticViewSitemap(sitemaps.Sitemap):
+ priority = 0.5
+ changefreq = "daily"
+
+ def items(self):
+ return ["home", "cette_semaine", "ce_mois_ci", "aujourdhui", "a_venir", "about", "contact"]
+
+ def location(self, item):
+ return reverse(item)
\ No newline at end of file
diff --git a/src/agenda_culturel/static/images/capture.png b/src/agenda_culturel/static/images/capture.png
deleted file mode 100644
index 203a3de..0000000
Binary files a/src/agenda_culturel/static/images/capture.png and /dev/null differ
diff --git a/src/agenda_culturel/static/js/modal.js b/src/agenda_culturel/static/js/modal.js
index 0182a5d..31a87cd 100644
--- a/src/agenda_culturel/static/js/modal.js
+++ b/src/agenda_culturel/static/js/modal.js
@@ -34,11 +34,17 @@ const openModal = (modal, back=true) => {
}
setTimeout(function() {
visibleModal = modal;
- }, 500);
+
+ console.log("ici");
+ const mask = visibleModal.querySelector(".h-mask");
+ mask.classList.add("visible");
+ }, 350);
};
const hideModal = (modal) => {
if (modal != null) {
+ const mask = visibleModal.querySelector(".h-mask");
+ mask.classList.remove("visible");
visibleModal = null;
document.documentElement.style.removeProperty("--scrollbar-width");
modal.removeAttribute("open");
diff --git a/src/agenda_culturel/static/location_field/js/form.js b/src/agenda_culturel/static/location_field/js/form.js
new file mode 100644
index 0000000..85f9cc3
--- /dev/null
+++ b/src/agenda_culturel/static/location_field/js/form.js
@@ -0,0 +1,673 @@
+var SequentialLoader = function() {
+ var SL = {
+ loadJS: function(src, onload) {
+ //console.log(src);
+ // add to pending list
+ this._load_pending.push({'src': src, 'onload': onload});
+ // check if not already loading
+ if ( ! this._loading) {
+ this._loading = true;
+ // load first
+ this.loadNextJS();
+ }
+ },
+
+ loadNextJS: function() {
+ // get next
+ var next = this._load_pending.shift();
+ if (next == undefined) {
+ // nothing to load
+ this._loading = false;
+ return;
+ }
+ // check not loaded
+ if (this._load_cache[next.src] != undefined) {
+ next.onload();
+ this.loadNextJS();
+ return; // already loaded
+ }
+ else {
+ this._load_cache[next.src] = 1;
+ }
+ // load
+ var el = document.createElement('script');
+ el.type = 'application/javascript';
+ el.src = next.src;
+ // onload callback
+ var self = this;
+ el.onload = function(){
+ //console.log('Loaded: ' + next.src);
+ // trigger onload
+ next.onload();
+ // try to load next
+ self.loadNextJS();
+ };
+ document.body.appendChild(el);
+ },
+
+ _loading: false,
+ _load_pending: [],
+ _load_cache: {}
+ };
+
+ return {
+ loadJS: SL.loadJS.bind(SL)
+ }
+};
+
+
+!function($){
+ var LocationFieldCache = {
+ load: [],
+ onload: {},
+
+ isLoading: false
+ };
+
+ var LocationFieldResourceLoader;
+
+ $.locationField = function(options) {
+ var LocationField = {
+ options: $.extend({
+ provider: 'google',
+ providerOptions: {
+ google: {
+ api: '//maps.google.com/maps/api/js',
+ mapType: 'ROADMAP'
+ }
+ },
+ searchProvider: 'google',
+ id: 'map',
+ latLng: '0,0',
+ mapOptions: {
+ zoom: 9
+ },
+ basedFields: $(),
+ inputField: $(),
+ suffix: '',
+ path: '',
+ fixMarker: true
+ }, options),
+
+ providers: /google|openstreetmap|mapbox/,
+ searchProviders: /google|yandex|nominatim|addok/,
+
+ render: function() {
+ this.$id = $('#' + this.options.id);
+
+ if ( ! this.providers.test(this.options.provider)) {
+ this.error('render failed, invalid map provider: ' + this.options.provider);
+ return;
+ }
+
+ if ( ! this.searchProviders.test(this.options.searchProvider)) {
+ this.error('render failed, invalid search provider: ' + this.options.searchProvider);
+ return;
+ }
+
+ var self = this;
+
+ this.loadAll(function(){
+ var mapOptions = self._getMapOptions(),
+ map = self._getMap(mapOptions);
+
+ var marker = self._getMarker(map, mapOptions.center);
+
+ // fix issue w/ marker not appearing
+ if (self.options.provider == 'google' && self.options.fixMarker)
+ self.__fixMarker();
+
+ // watch based fields
+ self._watchBasedFields(map, marker);
+ });
+ },
+
+ fill: function(latLng) {
+ this.options.inputField.val(latLng.lat + ',' + latLng.lng);
+ },
+
+ search: function(map, marker, address) {
+ if (this.options.searchProvider === 'google') {
+ var provider = new GeoSearch.GoogleProvider({ apiKey: this.options.providerOptions.google.apiKey });
+ provider.search({query: address}).then(data => {
+ if (data.length > 0) {
+ var result = data[0],
+ latLng = new L.LatLng(result.y, result.x);
+
+ marker.setLatLng(latLng);
+ map.panTo(latLng);
+ }
+ });
+ }
+
+ else if (this.options.searchProvider === 'yandex') {
+ // https://yandex.com/dev/maps/geocoder/doc/desc/concepts/input_params.html
+ var url = 'https://geocode-maps.yandex.ru/1.x/?format=json&geocode=' + address;
+
+ if (typeof this.options.providerOptions.yandex.apiKey !== 'undefined') {
+ url += '&apikey=' + this.options.providerOptions.yandex.apiKey;
+ }
+
+ var request = new XMLHttpRequest();
+ request.open('GET', url, true);
+
+ request.onload = function () {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ var pos = data.response.GeoObjectCollection.featureMember[0].GeoObject.Point.pos.split(' ');
+ var latLng = new L.LatLng(pos[1], pos[0]);
+ marker.setLatLng(latLng);
+ map.panTo(latLng);
+ } else {
+ console.error('Yandex geocoder error response');
+ }
+ };
+
+ request.onerror = function () {
+ console.error('Check connection to Yandex geocoder');
+ };
+
+ request.send();
+ }
+
+ else if (this.options.searchProvider === 'addok') {
+ var url = 'https://api-adresse.data.gouv.fr/search/?limit=1&q=' + address;
+
+ var request = new XMLHttpRequest();
+ request.open('GET', url, true);
+
+ request.onload = function () {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ var pos = data.features[0].geometry.coordinates;
+ var latLng = new L.LatLng(pos[1], pos[0]);
+ marker.setLatLng(latLng);
+ map.panTo(latLng);
+ } else {
+ console.error('Addok geocoder error response');
+ }
+ };
+
+ request.onerror = function () {
+ console.error('Check connection to Addok geocoder');
+ };
+
+ request.send();
+ }
+
+ else if (this.options.searchProvider === 'nominatim') {
+ var url = '//nominatim.openstreetmap.org/search?format=json&q=' + address;
+
+ var request = new XMLHttpRequest();
+ request.open('GET', url, true);
+
+ request.onload = function () {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ if (data.length > 0) {
+ var pos = data[0];
+ var latLng = new L.LatLng(pos.lat, pos.lon);
+ marker.setLatLng(latLng);
+ map.panTo(latLng);
+ } else {
+ console.error(address + ': not found via Nominatim');
+ }
+ } else {
+ console.error('Nominatim geocoder error response');
+ }
+ };
+
+ request.onerror = function () {
+ console.error('Check connection to Nominatim geocoder');
+ };
+
+ request.send();
+ }
+ },
+
+ loadAll: function(onload) {
+ this.$id.html('Loading...');
+
+ // resource loader
+ if (LocationFieldResourceLoader == undefined)
+ LocationFieldResourceLoader = SequentialLoader();
+
+ this.load.loader = LocationFieldResourceLoader;
+ this.load.path = this.options.path;
+
+ var self = this;
+
+ this.load.common(function(){
+ var mapProvider = self.options.provider,
+ onLoadMapProvider = function() {
+ var searchProvider = self.options.searchProvider + 'SearchProvider',
+ onLoadSearchProvider = function() {
+ self.$id.html('');
+ onload();
+ };
+
+ if (self.load[searchProvider] != undefined) {
+ self.load[searchProvider](self.options.providerOptions[self.options.searchProvider] || {}, onLoadSearchProvider);
+ }
+ else {
+ onLoadSearchProvider();
+ }
+ };
+
+ if (self.load[mapProvider] != undefined) {
+ self.load[mapProvider](self.options.providerOptions[mapProvider] || {}, onLoadMapProvider);
+ }
+ else {
+ onLoadMapProvider();
+ }
+ });
+ },
+
+ load: {
+ google: function(options, onload) {
+ var js = [
+ this.path + '/@googlemaps/js-api-loader/index.min.js',
+ this.path + '/Leaflet.GoogleMutant.js',
+ ];
+
+ this._loadJSList(js, function(){
+ const loader = new google.maps.plugins.loader.Loader({
+ apiKey: options.apiKey,
+ version: "weekly",
+ });
+ loader.load().then(() => onload());
+ });
+ },
+
+ googleSearchProvider: function(options, onload) {
+ onload();
+ //var url = options.api;
+
+ //if (typeof options.apiKey !== 'undefined') {
+ // url += url.indexOf('?') === -1 ? '?' : '&';
+ // url += 'key=' + options.apiKey;
+ //}
+
+ //var js = [
+ // url,
+ // this.path + '/l.geosearch.provider.google.js'
+ // ];
+
+ //this._loadJSList(js, function(){
+ // // https://github.com/smeijer/L.GeoSearch/issues/57#issuecomment-148393974
+ // L.GeoSearch.Provider.Google.Geocoder = new google.maps.Geocoder();
+
+ // onload();
+ //});
+ },
+
+ yandexSearchProvider: function (options, onload) {
+ onload();
+ },
+
+ mapbox: function(options, onload) {
+ onload();
+ },
+
+ openstreetmap: function(options, onload) {
+ onload();
+ },
+
+ common: function(onload) {
+ var self = this,
+ js = [
+ // map providers
+ this.path + '/leaflet/leaflet.js',
+ // search providers
+ this.path + '/leaflet-geosearch/geosearch.umd.js',
+ ],
+ css = [
+ // map providers
+ this.path + '/leaflet/leaflet.css'
+ ];
+
+ // Leaflet docs note:
+ // Include Leaflet JavaScript file *after* Leaflet’s CSS
+ // https://leafletjs.com/examples/quick-start/
+ this._loadCSSList(css, function(){
+ self._loadJSList(js, onload);
+ });
+ },
+
+ _loadJS: function(src, onload) {
+ this.loader.loadJS(src, onload);
+ },
+
+ _loadJSList: function(srclist, onload) {
+ this.__loadList(this._loadJS, srclist, onload);
+ },
+
+ _loadCSS: function(src, onload) {
+ if (LocationFieldCache.onload[src] != undefined) {
+ onload();
+ }
+ else {
+ LocationFieldCache.onload[src] = 1;
+ onloadCSS(loadCSS(src), onload);
+ }
+ },
+
+ _loadCSSList: function(srclist, onload) {
+ this.__loadList(this._loadCSS, srclist, onload);
+ },
+
+ __loadList: function(fn, srclist, onload) {
+ if (srclist.length > 1) {
+ for (var i = 0; i < srclist.length-1; ++i) {
+ fn.call(this, srclist[i], function(){});
+ }
+ }
+
+ fn.call(this, srclist[srclist.length-1], onload);
+ }
+ },
+
+ error: function(message) {
+ console.log(message);
+ this.$id.html(message);
+ },
+
+ _getMap: function(mapOptions) {
+ var map = new L.Map(this.options.id, mapOptions), layer;
+
+ if (this.options.provider == 'google') {
+ layer = new L.gridLayer.googleMutant({
+ type: this.options.providerOptions.google.mapType.toLowerCase(),
+ });
+ }
+ else if (this.options.provider == 'openstreetmap') {
+ layer = new L.tileLayer(
+ '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 18
+ });
+ }
+ else if (this.options.provider == 'mapbox') {
+ layer = new L.tileLayer(
+ 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
+ maxZoom: 18,
+ accessToken: this.options.providerOptions.mapbox.access_token,
+ id: 'mapbox/streets-v11'
+ });
+ }
+
+ map.addLayer(layer);
+
+ return map;
+ },
+
+ _getMapOptions: function() {
+ return $.extend(this.options.mapOptions, {
+ center: this._getLatLng()
+ });
+ },
+
+ _getLatLng: function() {
+ var l = this.options.latLng.split(',').map(parseFloat);
+ return new L.LatLng(l[0], l[1]);
+ },
+
+ _getMarker: function(map, center) {
+ var self = this,
+ markerOptions = {
+ draggable: true
+ };
+
+ var marker = L.marker(center, markerOptions).addTo(map);
+
+ marker.on('dragstart', function(){
+ if (self.options.inputField.is('[readonly]'))
+ marker.dragging.disable();
+ else
+ marker.dragging.enable();
+ });
+
+ // fill input on dragend
+ marker.on('dragend move', function(){
+ if (!self.options.inputField.is('[readonly]'))
+ self.fill(this.getLatLng());
+ });
+
+ // place marker on map click
+ map.on('click', function(e){
+ if (!self.options.inputField.is('[readonly]')) {
+ marker.setLatLng(e.latlng);
+ marker.dragging.enable();
+ }
+ });
+
+ return marker;
+ },
+
+ _watchBasedFields: function(map, marker) {
+ var self = this,
+ basedFields = this.options.basedFields,
+ onchangeTimer,
+ onchange = function() {
+ if (!self.options.inputField.is('[readonly]')) {
+ var values = basedFields.map(function() {
+ var value = $(this).val();
+ return value === '' ? null : value;
+ });
+ var address = values.toArray().join(', ');
+ clearTimeout(onchangeTimer);
+ onchangeTimer = setTimeout(function(){
+ self.search(map, marker, address);
+ }, 300);
+ }
+ };
+
+ basedFields.each(function(){
+ var el = $(this);
+
+ if (el.is('select'))
+ el.change(onchange);
+ else
+ el.keyup(onchange);
+ });
+
+ if (this.options.inputField.val() === '') {
+ var values = basedFields.map(function() {
+ var value = $(this).val();
+ return value === '' ? null : value;
+ });
+ var address = values.toArray().join(', ');
+ if (address !== '')
+ onchange();
+ }
+ },
+
+ __fixMarker: function() {
+ $('.leaflet-map-pane').css('z-index', '2 !important');
+ $('.leaflet-google-layer').css('z-index', '1 !important');
+ }
+ }
+
+ return {
+ render: LocationField.render.bind(LocationField)
+ }
+ }
+
+ function dataLocationFieldObserver(callback) {
+ function _findAndEnableDataLocationFields() {
+ var dataLocationFields = $('input[data-location-field-options]');
+
+ dataLocationFields
+ .filter(':not([data-location-field-observed])')
+ .attr('data-location-field-observed', true)
+ .each(callback);
+ }
+
+ var observer = new MutationObserver(function(mutations){
+ _findAndEnableDataLocationFields();
+ });
+
+ var container = document.documentElement || document.body;
+
+ $(container).ready(function(){
+ _findAndEnableDataLocationFields();
+ });
+
+ observer.observe(container, {attributes: true});
+ }
+
+ dataLocationFieldObserver(function(){
+ var el = $(this);
+
+ var name = el.attr('name'),
+ options = el.data('location-field-options'),
+ basedFields = options.field_options.based_fields,
+ pluginOptions = {
+ id: 'map_' + name,
+ inputField: el,
+ latLng: el.val() || '0,0',
+ suffix: options['search.suffix'],
+ path: options['resources.root_path'],
+ provider: options['map.provider'],
+ searchProvider: options['search.provider'],
+ providerOptions: {
+ google: {
+ api: options['provider.google.api'],
+ apiKey: options['provider.google.api_key'],
+ mapType: options['provider.google.map_type']
+ },
+ mapbox: {
+ access_token: options['provider.mapbox.access_token']
+ },
+ yandex: {
+ apiKey: options['provider.yandex.api_key']
+ },
+ },
+ mapOptions: {
+ zoom: options['map.zoom']
+ }
+ };
+
+ // prefix
+ var prefixNumber;
+
+ try {
+ prefixNumber = name.match(/-(\d+)-/)[1];
+ } catch (e) {}
+
+ if (options.field_options.prefix) {
+ var prefix = options.field_options.prefix;
+
+ if (prefixNumber != null) {
+ prefix = prefix.replace(/__prefix__/, prefixNumber);
+ }
+
+ basedFields = basedFields.map(function(n){
+ return prefix + n
+ });
+ }
+
+ // based fields
+ pluginOptions.basedFields = $(basedFields.map(function(n){
+ return '#id_' + n
+ }).join(','));
+
+ // render
+ $.locationField(pluginOptions).render();
+ });
+
+}(jQuery || django.jQuery);
+
+/*!
+loadCSS: load a CSS file asynchronously.
+[c]2015 @scottjehl, Filament Group, Inc.
+Licensed MIT
+*/
+(function(w){
+ "use strict";
+ /* exported loadCSS */
+ var loadCSS = function( href, before, media ){
+ // Arguments explained:
+ // `href` [REQUIRED] is the URL for your CSS file.
+ // `before` [OPTIONAL] is the element the script should use as a reference for injecting our stylesheet before
+ // By default, loadCSS attempts to inject the link after the last stylesheet or script in the DOM. However, you might desire a more specific location in your document.
+ // `media` [OPTIONAL] is the media type or query of the stylesheet. By default it will be 'all'
+ var doc = w.document;
+ var ss = doc.createElement( "link" );
+ var ref;
+ if( before ){
+ ref = before;
+ }
+ else {
+ var refs = ( doc.body || doc.getElementsByTagName( "head" )[ 0 ] ).childNodes;
+ ref = refs[ refs.length - 1];
+ }
+
+ var sheets = doc.styleSheets;
+ ss.rel = "stylesheet";
+ ss.href = href;
+ // temporarily set media to something inapplicable to ensure it'll fetch without blocking render
+ ss.media = "only x";
+
+ // Inject link
+ // Note: the ternary preserves the existing behavior of "before" argument, but we could choose to change the argument to "after" in a later release and standardize on ref.nextSibling for all refs
+ // Note: `insertBefore` is used instead of `appendChild`, for safety re: http://www.paulirish.com/2011/surefire-dom-element-insertion/
+ ref.parentNode.insertBefore( ss, ( before ? ref : ref.nextSibling ) );
+ // A method (exposed on return object for external use) that mimics onload by polling until document.styleSheets until it includes the new sheet.
+ var onloadcssdefined = function( cb ){
+ var resolvedHref = ss.href;
+ var i = sheets.length;
+ while( i-- ){
+ if( sheets[ i ].href === resolvedHref ){
+ return cb();
+ }
+ }
+ setTimeout(function() {
+ onloadcssdefined( cb );
+ });
+ };
+
+ // once loaded, set link's media back to `all` so that the stylesheet applies once it loads
+ ss.onloadcssdefined = onloadcssdefined;
+ onloadcssdefined(function() {
+ ss.media = media || "all";
+ });
+ return ss;
+ };
+ // commonjs
+ if( typeof module !== "undefined" ){
+ module.exports = loadCSS;
+ }
+ else {
+ w.loadCSS = loadCSS;
+ }
+}( typeof global !== "undefined" ? global : this ));
+
+
+/*!
+onloadCSS: adds onload support for asynchronous stylesheets loaded with loadCSS.
+[c]2014 @zachleat, Filament Group, Inc.
+Licensed MIT
+*/
+
+/* global navigator */
+/* exported onloadCSS */
+function onloadCSS( ss, callback ) {
+ ss.onload = function() {
+ ss.onload = null;
+ if( callback ) {
+ callback.call( ss );
+ }
+ };
+
+ // This code is for browsers that don’t support onload, any browser that
+ // supports onload should use that instead.
+ // No support for onload:
+ // * Android 4.3 (Samsung Galaxy S4, Browserstack)
+ // * Android 4.2 Browser (Samsung Galaxy SIII Mini GT-I8200L)
+ // * Android 2.3 (Pantech Burst P9070)
+
+ // Weak inference targets Android < 4.4
+ if( "isApplicationInstalled" in navigator && "onloadcssdefined" in ss ) {
+ ss.onloadcssdefined( callback );
+ }
+}
diff --git a/src/agenda_culturel/static/style.scss b/src/agenda_culturel/static/style.scss
index 4947696..f13b9d3 100644
--- a/src/agenda_culturel/static/style.scss
+++ b/src/agenda_culturel/static/style.scss
@@ -44,6 +44,9 @@ $enable-responsive-typography: true;
// Modal (