Merge branch 'main' into correction-script-import-comedie
This commit is contained in:
commit
cc0ae8b582
@ -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
|
||||
|
@ -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 \
|
||||
|
43
experimentations/get_le_rio.py
Executable file
43
experimentations/get_le_rio.py
Executable file
@ -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))
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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"]
|
||||
|
||||
|
||||
|
@ -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):
|
||||
'<li><a href="' + e.get_absolute_url() + '">' + e.title + "</a></li>"
|
||||
)
|
||||
result += (
|
||||
"<li>Création : " + localize(localtime(e.created_date)) + "</li>"
|
||||
"<li>Création : " + localize(e.created_date) + "</li>"
|
||||
)
|
||||
result += (
|
||||
"<li>Dernière modification : "
|
||||
+ localize(localtime(e.modified_date))
|
||||
+ localize(e.modified_date)
|
||||
+ "</li>"
|
||||
)
|
||||
if e.imported_date:
|
||||
result += (
|
||||
"<li>Dernière importation : "
|
||||
+ localize(localtime(e.imported_date))
|
||||
+ localize(e.imported_date)
|
||||
+ "</li>"
|
||||
)
|
||||
result += "</ul>"
|
||||
@ -602,7 +610,7 @@ class MergeDuplicates(Form):
|
||||
result += '<input id="' + id + '" name="' + key + '"'
|
||||
if key in MergeDuplicates.checkboxes_fields:
|
||||
result += ' type="checkbox"'
|
||||
if value in checked:
|
||||
if checked and value in checked:
|
||||
result += " checked"
|
||||
else:
|
||||
result += ' type="radio"'
|
||||
@ -634,7 +642,7 @@ class MergeDuplicates(Form):
|
||||
result = []
|
||||
for s in selected:
|
||||
for e in self.duplicates.get_duplicated():
|
||||
if e.pk == selected:
|
||||
if e.pk == s:
|
||||
result.append(e)
|
||||
break
|
||||
return result
|
||||
@ -718,7 +726,7 @@ class EventAddPlaceForm(Form):
|
||||
return self.instance
|
||||
|
||||
|
||||
class PlaceForm(ModelForm):
|
||||
class PlaceForm(GroupFormMixin, ModelForm):
|
||||
required_css_class = 'required'
|
||||
|
||||
apply_to_all = BooleanField(
|
||||
@ -734,24 +742,70 @@ class PlaceForm(ModelForm):
|
||||
fields = "__all__"
|
||||
widgets = {"location": TextInput()}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.add_group('header', _('Header'))
|
||||
self.fields['name'].group_id = 'header'
|
||||
|
||||
|
||||
self.add_group('address', _('Address'))
|
||||
self.fields['address'].group_id = 'address'
|
||||
self.fields['postcode'].group_id = 'address'
|
||||
self.fields['city'].group_id = 'address'
|
||||
self.fields['location'].group_id = 'address'
|
||||
|
||||
self.add_group('meta', _('Meta'))
|
||||
self.fields['aliases'].group_id = 'meta'
|
||||
|
||||
self.add_group('information', _('Information'))
|
||||
self.fields['description'].group_id = 'information'
|
||||
|
||||
def as_grid(self):
|
||||
return mark_safe(
|
||||
'<div class="grid"><div>'
|
||||
result = ('<div class="grid"><div>'
|
||||
+ super().as_p()
|
||||
+ '</div><div><div class="map-widget">'
|
||||
+ '<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div><p>Cliquez pour ajuster la position GPS</p></div></div></div>'
|
||||
)
|
||||
+ '''</div><div><div class="map-widget">
|
||||
<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div>
|
||||
<p>Cliquez pour ajuster la position GPS</p></div>
|
||||
<input type="checkbox" role="switch" id="lock_position">Verrouiller la position</lock>
|
||||
<script>
|
||||
document.getElementById("lock_position").onclick = function() {
|
||||
const field = document.getElementById("id_location");
|
||||
if (this.checked)
|
||||
field.setAttribute("readonly", true);
|
||||
else
|
||||
field.removeAttribute("readonly");
|
||||
}
|
||||
</script>
|
||||
</div></div>''')
|
||||
|
||||
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")
|
@ -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("<!-- " + self.url + " -->\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")
|
||||
|
||||
|
||||
|
91
src/agenda_culturel/import_tasks/custom_extractors/lerio.py
Normal file
91
src/agenda_culturel/import_tasks/custom_extractors/lerio.py
Normal file
@ -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
|
||||
)
|
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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"])
|
||||
|
||||
|
||||
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
18
src/agenda_culturel/migrations/0124_place_postcode.py
Normal file
18
src/agenda_culturel/migrations/0124_place_postcode.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
@ -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'},
|
||||
),
|
||||
]
|
21
src/agenda_culturel/migrations/0126_message_user.py
Normal file
21
src/agenda_culturel/migrations/0126_message_user.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
18
src/agenda_culturel/migrations/0128_event_datetimes_title.py
Normal file
18
src/agenda_culturel/migrations/0128_event_datetimes_title.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
||||
|
@ -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")),
|
||||
|
13
src/agenda_culturel/sitemaps.py
Normal file
13
src/agenda_culturel/sitemaps.py
Normal file
@ -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)
|
Binary file not shown.
Before Width: | Height: | Size: 237 KiB |
@ -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");
|
||||
|
673
src/agenda_culturel/static/location_field/js/form.js
Normal file
673
src/agenda_culturel/static/location_field/js/form.js
Normal file
@ -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 <link> 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 );
|
||||
}
|
||||
}
|
@ -44,6 +44,9 @@ $enable-responsive-typography: true;
|
||||
// Modal (<dialog>)
|
||||
--modal-overlay-backdrop-filter: blur(0.05rem);
|
||||
|
||||
--background-color-transparent: color-mix(in srgb, var(--background-color), transparent 30%);
|
||||
|
||||
--background-color-transparent-light: color-mix(in srgb, var(--background-color), transparent 80%);
|
||||
}
|
||||
|
||||
|
||||
@ -329,6 +332,7 @@ footer [data-tooltip] {
|
||||
scroll-behavior: smooth;
|
||||
transition-duration: 200ms;
|
||||
|
||||
|
||||
.cat {
|
||||
margin-right: 0;
|
||||
}
|
||||
@ -1434,6 +1438,17 @@ img.preview {
|
||||
}
|
||||
}
|
||||
|
||||
.header-complement {
|
||||
float: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
.header-complement {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
form.messages div, form.moderation-events {
|
||||
@media only screen and (min-width: 992px) {
|
||||
display: grid;
|
||||
@ -1454,6 +1469,11 @@ form.messages div, form.moderation-events {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#moderate-form #id_status {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
label.required::after {
|
||||
content: ' *';
|
||||
color: red;
|
||||
@ -1502,3 +1522,228 @@ label.required::after {
|
||||
.maskable_group .body_group.closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-place {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
row-gap: .5em;
|
||||
margin-bottom: 0.5em;
|
||||
.map-widget {
|
||||
grid-row: 3;
|
||||
}
|
||||
#group_address .body_group {
|
||||
display: grid;
|
||||
grid-template-columns: repear(2, 1fr);
|
||||
|
||||
column-gap: .5em;
|
||||
#div_id_address, #div_id_location {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 992px) {
|
||||
.form-place {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
.map-widget {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
#group_other {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-now {
|
||||
font-size: 60%;
|
||||
div {
|
||||
display: grid;
|
||||
grid-template-columns: fit-content(2em) auto;
|
||||
column-gap: .2em;
|
||||
color: red;
|
||||
.line {
|
||||
margin-top: .7em;
|
||||
border-top: 1px solid red;
|
||||
}
|
||||
}
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.a-venir .line-now {
|
||||
margin-left: -2em;
|
||||
|
||||
}
|
||||
|
||||
#chronology {
|
||||
.entree {
|
||||
display: grid;
|
||||
grid-template-columns: fit-content(2em) auto;
|
||||
column-gap: .7em;
|
||||
.texte {
|
||||
background: var(--background-color);
|
||||
padding: 0.1em 0.8em;
|
||||
border-radius: var(--border-radius);
|
||||
p {
|
||||
font-size: 100%;
|
||||
}
|
||||
p:last-child {
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
font-size: 85%;
|
||||
footer {
|
||||
margin-top: 1.8em;
|
||||
padding: 0.2em .8em;
|
||||
}
|
||||
.ts {
|
||||
@extend .badge-small;
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
width: 14em;
|
||||
margin-right: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.moderation_heatmap {
|
||||
overflow-x: auto;
|
||||
table {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
.total, .month {
|
||||
display: none;
|
||||
}
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
td {
|
||||
font-size: 80%;
|
||||
text-align: center;
|
||||
}
|
||||
tbody th {
|
||||
text-align: right;
|
||||
}
|
||||
.ratio {
|
||||
padding: 0.1em;
|
||||
a, .a {
|
||||
margin: auto;
|
||||
border-radius: var(--border-radius);
|
||||
color: black;
|
||||
padding: 0;
|
||||
display: block;
|
||||
max-width: 6em;
|
||||
width: 3.2em;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.score_0 {
|
||||
a, .a {
|
||||
background: rgb(0, 128, 0);
|
||||
}
|
||||
a:hover {
|
||||
background: rgb(0, 176, 0);
|
||||
}
|
||||
}
|
||||
.score_1 {
|
||||
a, .a {
|
||||
background: rgb(255, 255, 0);
|
||||
}
|
||||
a:hover {
|
||||
background: rgb(248, 248, 121);
|
||||
}
|
||||
}
|
||||
.score_2 {
|
||||
a, .a {
|
||||
background: rgb(255, 166, 0);
|
||||
}
|
||||
a:hover {
|
||||
background: rgb(255, 182, 47);
|
||||
}
|
||||
}
|
||||
.score_3 {
|
||||
a, .a {
|
||||
background: rgb(255, 0, 0);
|
||||
}
|
||||
a:hover {
|
||||
background: rgb(255, 91, 91);
|
||||
}
|
||||
}
|
||||
.score_4 {
|
||||
a, .a {
|
||||
background: rgb(128, 0, 128);
|
||||
color: white;
|
||||
}
|
||||
a:hover {
|
||||
background: rgb(178, 0, 178);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1800px) {
|
||||
.total, .month {
|
||||
display: inline;
|
||||
opacity: .35;
|
||||
}
|
||||
.label {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1600px) {
|
||||
.ratio {
|
||||
a, .a {
|
||||
width: 5em;
|
||||
height: 3.2em;
|
||||
line-height: 3.2em;
|
||||
}
|
||||
}
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog {
|
||||
.h-image {
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center, center;
|
||||
}
|
||||
|
||||
.h-mask {
|
||||
background-color: var(--background-color);
|
||||
margin: calc(var(--spacing) * -1.5);
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
.h-mask.visible {
|
||||
background-color: var(--background-color-transparent);
|
||||
transition: background-color .8s ease-in;
|
||||
}
|
||||
.h-mask.visible:hover {
|
||||
background-color: var(--background-color-transparent-light);
|
||||
}
|
||||
}
|
||||
|
||||
.visible-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.detail-link {
|
||||
text-align: right;
|
||||
padding-right: 0.4em;
|
||||
.visible-link {
|
||||
color: var(--contrast);
|
||||
}
|
||||
}
|
||||
.week-in-month {
|
||||
article {
|
||||
.visible-link {
|
||||
color: var(--contrast);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,13 +17,53 @@
|
||||
{% block content %}
|
||||
<div class="grid two-columns">
|
||||
<div id="contenu-principal">
|
||||
<div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<div class="slide-buttons">
|
||||
<a href="{% url 'moderate' %}" role="button">Modérer {% picto_from_name "check-square" %}</a>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<div class="slide-buttons">
|
||||
<a href="{% url 'moderate' %}" role="button">Modérer {% picto_from_name "check-square" %}</a>
|
||||
</div>
|
||||
<h2>Modération à venir</h2>
|
||||
</header>
|
||||
<div class="grid">
|
||||
<div>
|
||||
{% url 'administration' as local_url %}
|
||||
{% include "agenda_culturel/static_content.html" with name="administration" url_path=local_url %}
|
||||
</div>
|
||||
<div class="moderation_heatmap">
|
||||
{% for w in nb_not_moderated %}
|
||||
<table>
|
||||
<thead>
|
||||
<th class="label"></th>
|
||||
{% for m in w %}
|
||||
<th><a href="{% url 'day_view' m.start_day.year m.start_day.month m.start_day.day %}">{{ m.start_day|date:"d" }}<span class="month"> {{ m.start_day|date:"M"|lower }}</span></a></th>
|
||||
{% endfor %}
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="label">reste à modérer</h>
|
||||
{% for m in w %}
|
||||
<td class="ratio score_{{ m.note }}">
|
||||
<{% if m.not_moderated > 0 %}a href="{% if m.is_today %}
|
||||
{% url 'moderate' %}
|
||||
{% else %}
|
||||
{% url 'moderate_from_date' m.start_day.year m.start_day.month m.start_day.day %}
|
||||
{% endif %}"{% else %}span class="a"{% endif %}>
|
||||
{{ m.not_moderated }}<span class="total"> / {{ m.nb_events }}</span></{% if m.not_moderated > 0 %}a{% else %}span{% endif %}>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2>Activité des derniers jours</h2>
|
||||
</header>
|
||||
<h3>Résumé des activités</h3>
|
||||
@ -36,7 +76,8 @@
|
||||
{% include "agenda_culturel/rimports-info-inc.html" with all=1 %}</p>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<div class="slide-buttons">
|
||||
|
@ -0,0 +1,27 @@
|
||||
{% extends "agenda_culturel/page-admin.html" %}
|
||||
|
||||
|
||||
{% block fluid %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h1>{% block title %}{% block og_title %}Vider le cache{% endblock %}{% endblock %}</h1>
|
||||
</header>
|
||||
<form method="post">{% csrf_token %}
|
||||
<p>Êtes-vous sûr·e de vouloir vider le cache ? Toutes les pages seront
|
||||
générées lors de leur consultation, mais cela peut ralentir temporairemenet l'expérience de navigation.
|
||||
</p>
|
||||
{{ form }}
|
||||
|
||||
<footer>
|
||||
<div class="grid buttons">
|
||||
<a href="{{ cancel_url }}" role="button" class="secondary">Annuler</a>
|
||||
<input type="submit" value="Confirmer">
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
@ -12,7 +12,7 @@
|
||||
{% if local %}
|
||||
<a href="{{ local.get_absolute_url }}" role="button">voir la version locale {% picto_from_name "eye" %}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'clone_edit' event.id %}" role="button">créer une copie locale {% picto_from_name "plus-circle" %}</a>
|
||||
<a href="{% url 'clone_edit' event.id %}" role="button">modifier en copie locale {% picto_from_name "plus-circle" %}</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
|
@ -1,13 +1,13 @@
|
||||
{% if user.is_authenticated %}
|
||||
<p class="footer">Création : {{ event.created_date }}
|
||||
<p class="footer">Création : {{ event.created_date }}{% if event.created_by_user %} par <em>{{ event.created_by_user.username }}</em>{% endif %}
|
||||
{% if event.modified %}
|
||||
— dernière modification : {{ event.modified_date }}
|
||||
— dernière modification : {{ event.modified_date }}{% if event.modified_by_user %} par <em>{{ event.modified_by_user.username }}</em>{% endif %}
|
||||
{% endif %}
|
||||
{% if event.imported_date %}
|
||||
— dernière importation : {{ event.imported_date }}
|
||||
— dernière importation : {{ event.imported_date }}{% if event.imported_by_user %} par <em>{{ event.imported_by_user.username }}</em>{% endif %}
|
||||
{% endif %}
|
||||
{% if event.moderated_date %}
|
||||
— dernière modération : {{ event.moderated_date }}
|
||||
— dernière modération : {{ event.moderated_date }}{% if event.moderated_by_user %} par <em>{{ event.moderated_by_user.username }}</em>{% endif %}
|
||||
{% endif %}
|
||||
{% if event.pure_import %}
|
||||
— <strong>version importée</strong>
|
||||
|
@ -1,10 +1,12 @@
|
||||
<footer class="remarque">
|
||||
Informations complémentaires non éditables :
|
||||
<strong>Informations complémentaires non éditables</strong>
|
||||
<ul>
|
||||
{% if object.created_date %}<li>Création : {{ object.created_date }}</li>{% endif %}
|
||||
{% if object.modified_date %}<li>Dernière modification : {{ object.modified_date }}</li>{% endif %}
|
||||
{% if object.moderated_date %}<li>Dernière modération : {{ object.moderated_date }}</li>{% endif %}
|
||||
{% if object.imported_date %}<li>Dernière importation : {{ object.imported_date }}</li>{% endif %}
|
||||
{% if not allbutdates %}
|
||||
{% if object.created_date %}<li>Création : {{ object.created_date }}{% if object.created_by_user %} par <em>{{ object.created_by_user.username }}</em>{% endif %}</li>{% endif %}
|
||||
{% if object.modified_date %}<li>Dernière modification : {{ object.modified_date }}{% if object.modified_by_user %} par <em>{{ object.modified_by_user.username }}</em>{% endif %}</li>{% endif %}
|
||||
{% if object.moderated_date %}<li>Dernière modération : {{ object.moderated_date }}{% if object.moderated_by_user %} par <em>{{ object.moderated_by_user.username }}</em>{% endif %}</li>{% endif %}
|
||||
{% if object.imported_date %}<li>Dernière importation : {{ object.imported_date }}{% if object.imported_by_user %} par <em>{{ object.imported_by_user.username }}</em>{% endif %}</li>{% endif %}
|
||||
{% endif %}
|
||||
{% if object.uuids %}
|
||||
{% if object.uuids|length > 0 %}
|
||||
<li>UUIDs (identifiants uniques d'événements dans les sources) :
|
||||
|
@ -81,7 +81,7 @@ Duplication de {% else %}
|
||||
{{ form }}
|
||||
<div class="grid buttons stick-bottom">
|
||||
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
|
||||
<input type="submit" value="Enregistrer">
|
||||
<input type="submit" value="Enregistrer{% if form.is_clone_from_url %} et modérer{% endif %}">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -33,31 +33,39 @@
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">{% csrf_token %}
|
||||
<form method="post" enctype="multipart/form-data" id="moderate-form">{% csrf_token %}
|
||||
|
||||
<div class="grid moderate-preview">
|
||||
|
||||
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event noedit=1 %}
|
||||
<div>
|
||||
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event onlyedit=1 %}
|
||||
|
||||
{% with event.get_concurrent_events as concurrent_events %}
|
||||
{% if concurrent_events %}
|
||||
<article>
|
||||
<header>
|
||||
<h2>En même temps</h2>
|
||||
<p class="remarque">{% if concurrent_events|length > 1 %}Plusieurs événements se déroulent en même temps.{% else %}Un autre événement se déroule en même temps.{% endif %}</p>
|
||||
</header>
|
||||
<ul>
|
||||
{% for e in concurrent_events %}
|
||||
<li>
|
||||
{{ e.category|circle_cat }} {% if e.start_time %}{{ e.start_time }}{% else %}<em>toute la journée</em>{% endif %} <a href="{{ e.get_absolute_url }}">{{ e.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<div class="slide-buttons">
|
||||
{% with event.get_local_version as local %}
|
||||
{% if local %}
|
||||
{% if event != local %}
|
||||
<input type="submit" value="Enregistrer et éditer la version locale" name="save_and_edit_local">
|
||||
{% else %}
|
||||
<input type="submit" value="Enregistrer et éditer" name="save_and_edit">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<input type="submit" value="Enregistrer et créer une version locale" name="save_and_create_local">
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<h2>Modification des méta-informations</h2>
|
||||
{% if event.moderated_date %}
|
||||
<p class="message info">Cet événement a déjà été modéré par le {{ event.moderated_date }}.
|
||||
<p class="message info">Cet événement a déjà été modéré {% if event.moderation_by_user %}par {<em>{ event.moderation_by_user.username }}</em> {% endif %}le {{ event.moderated_date }}.
|
||||
Vous pouvez bien sûr modifier de nouveau ces méta-informations en utilisant
|
||||
le formulaire ci-après.
|
||||
</p>
|
||||
@ -69,23 +77,13 @@
|
||||
</div>
|
||||
<div class="grid buttons">
|
||||
{% if pred %}
|
||||
<a href="{% url 'moderate_event' pred %}" role="button">🠄 Revenir au précédent</a>
|
||||
<a href="{% url 'moderate_event' pred %}" class="secondary" role="button">< Revenir au précédent</a>
|
||||
{% else %}
|
||||
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
|
||||
{% endif %}
|
||||
<input type="submit" value="Enregistrer" name="save">
|
||||
{% with event.get_local_version as local %}
|
||||
{% if local %}
|
||||
{% if local == event %}
|
||||
<input type="submit" value="Enregistrer et éditer la version locale" name="save_and_edit_local">
|
||||
{% else %}
|
||||
<input type="submit" value="Enregistrer et éditer" name="save_and_edit">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<input type="submit" value="Enregistrer et créer une version locale" name="save_and_create_local">
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<input type="submit" value="Enregistrer et passer au suivant 🠆" name="save_and_next">
|
||||
<input type="submit" value="Enregistrer et passer au suivant >" name="save_and_next">
|
||||
<a href="{% url 'moderate_event_next' event.pk %}" class="secondary" role="button">Passer au suivant sans enregistrer ></a>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% extends "agenda_culturel/page-admin.html" %}
|
||||
{% load static %}
|
||||
{% load honeypot %}
|
||||
|
||||
{% block title %}{% block og_title %}{% if form.event %}Contact au sujet de l'événement {{ form.event.title }}{% else %}
|
||||
Contact{% endif %}{% endblock %}{% endblock %}
|
||||
@ -31,7 +32,7 @@ Contact{% endif %}{% endblock %}{% endblock %}
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
<p class="message warning"><strong>Attention :</strong> n'utilisez pas le formulaire ci-dessous pour proposer un événement, il sera ignoré. Utilisez plutôt la page <a href="{% url 'add_event' %}">ajouter un événement</a>.</p>
|
||||
<p class="message warning"><strong>Attention :</strong> n'utilisez pas le formulaire ci-dessous pour proposer un événement, il sera ignoré. Utilisez plutôt la page <a href="{% url 'add_event' %}">ajouter un événement</a>.</p>
|
||||
{% if form.event %}
|
||||
<p>Tu nous contactes au sujet de l'événement « {{ form.event.title }} » du {{ form.event.start_day }}.
|
||||
N'hésites pas à nous indiquer le maximum de contexte et à nous laisser ton adresse
|
||||
@ -40,6 +41,7 @@ Contact{% endif %}{% endblock %}{% endblock %}
|
||||
{% endif %}
|
||||
</header>
|
||||
<form method="post">{% csrf_token %}
|
||||
{% render_honeypot_field "alias_name" %}
|
||||
{{ form.media }}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Envoyer">
|
@ -24,13 +24,13 @@
|
||||
<article>
|
||||
<header>
|
||||
<div class="slide-buttons">
|
||||
<a href="{% url 'delete_contactmessage' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
|
||||
<a href="{% url 'delete_message' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
|
||||
</div>
|
||||
|
||||
<h1>Modération du message « {{ object.subject }} »</h1>
|
||||
<ul>
|
||||
<li>Date : {{ object.date.date }} à {{ object.date.time }}</li>
|
||||
<li>Auteur : {{ object.name }} {% if object.email %}<a href="mailto:{{ object.email }}">{{ object.email }}</a>{% endif %}</li>
|
||||
<li>Auteur : {% if object.user %}<em>{{ object.user }}</em>{% else %}{{ object.name }}{% endif %} {% if object.email %}<a href="mailto:{{ object.email }}">{{ object.email }}</a>{% endif %}</li>
|
||||
{% if object.related_event %}<li>Événement associé : <a href="{{ object.related_event.get_absolute_url }}">{{ object.related_event.title }}</a> du {{ object.related_event.start_day }}</li>{% endif %}
|
||||
</ul>
|
||||
</header>
|
||||
@ -47,7 +47,7 @@
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
|
||||
{% include "agenda_culturel/side-nav.html" with current="messages" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -45,8 +45,8 @@
|
||||
{% for obj in paginator_filter %}
|
||||
<tr>
|
||||
<td>{{ obj.date }}</td>
|
||||
<td><a href="{% url 'contactmessage' obj.pk %}">{{ obj.subject }}</a></td>
|
||||
<td>{{ obj.name }}</td>
|
||||
<td><a href="{% url 'message' obj.pk %}">{{ obj.subject }}</a></td>
|
||||
<td>{% if obj.user %}<em>{{ obj.user }}</em>{% else %}{{ obj.name }}{% endif %}</td>
|
||||
<td>{% if obj.related_event %}<a href="{{ obj.related_event.get_absolute_url }}">{{ obj.related_event.pk }}</a>{% else %}/{% endif %}</td>
|
||||
<td>{% if obj.closed %}{% picto_from_name "check-square" "fermé" %}{% else %}{% picto_from_name "square" "ouvert" %}{% endif %}</td>
|
||||
<td>{% if obj.spam %}{% picto_from_name "check-square" "spam" %}{% else %}{% picto_from_name "square" "non spam" %}{% endif %}</td>
|
||||
@ -59,7 +59,7 @@
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
|
||||
{% include "agenda_culturel/side-nav.html" with current="messages" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -35,12 +35,8 @@
|
||||
const places = document.querySelector('#id_principal_place');
|
||||
const choices_places = new Choices(places,
|
||||
{
|
||||
placeholderValue: 'Sélectionner le lieu principal ',
|
||||
allowHTML: true,
|
||||
delimiter: ',',
|
||||
removeItemButton: true,
|
||||
shouldSort: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
|
@ -3,10 +3,11 @@
|
||||
{% load cat_extra %}
|
||||
{% load utils_extra %}
|
||||
{% load event_extra %}
|
||||
{% load cache %}
|
||||
|
||||
|
||||
{% block title %}{% block og_title %}{{ event.title }}{% endblock %}{% endblock %}
|
||||
{% block og_image %}{% if event.has_image_url %}{{ event.get_image_url }}{% else %}{{ block.super }}{% endif %}{% endblock %}
|
||||
{% block og_image %}{% if event.has_image_url %}{{ event|get_image_uri:request }}{% else %}{{ block.super }}{% endif %}{% endblock %}
|
||||
{% block og_description %}{% if event.description %}{{ event.description |truncatewords:20|linebreaks }}{% else %}{{ block.super }}{% endif %}{% endblock %}
|
||||
|
||||
{% block entete_header %}
|
||||
@ -16,21 +17,59 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
|
||||
<div class="grid two-columns">
|
||||
<div>
|
||||
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
|
||||
{% cache cache_timeout event_body user.is_authenticated event %}
|
||||
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event filter=filter %}
|
||||
{% endcache %}
|
||||
{% endwith %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<article>
|
||||
<article id="chronology">
|
||||
<header>
|
||||
<h2>Informations internes</h2>
|
||||
<h2>Chronologie</h2>
|
||||
</header>
|
||||
{% include "agenda_culturel/event-info-inc.html" with object=event %}
|
||||
{% for step in event.chronology %}
|
||||
{% if step.is_date %}
|
||||
<div class="entree dateline">
|
||||
<div><span class="ts">{{ step.timestamp }}</span></div>
|
||||
<div>
|
||||
{% if step.data == "created_date" %}<em>création</em>{% if event.created_by_user %} par {{ event.created_by_user.username }}{% endif %}{% endif %}
|
||||
{% if step.data == "modified_date" %}<em>dernière modification</em>{% if event.modified_by_user %} par {{ event.modified_by_user.username }}{% endif %}{% endif %}
|
||||
{% if step.data == "moderated_date" %}<em>dernière modération</em>{% if event.moderated_by_user %} par {{ event.moderated_by_user.username }}{% endif %}{% endif %}
|
||||
{% if step.data == "imported_date" %}<em>dernière importation</em>{% if event.imported_by_user %} par {{ event.imported_by_user.username }}{% endif %}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="entree">
|
||||
<div><span class="ts">{{ step.timestamp }}</span></div>
|
||||
<div>
|
||||
<header><strong>Message</strong>{% if step.data.related_event and event != step.data.related_event %} sur <a href="{{ step.data.related_event.get_absolute_url }}">une autre</a> version{% endif %} : <a href="{{ step.data.get_absolute_url }}">{{ step.data.subject|truncatechars:20 }}</a> {% if step.data.user %} par <em>{{ step.data.user }}</em>{% else %} par {{ step.data.name }}{% if step.data.email %} (<a href="mailto: {{ step.data.email }}">{{ step.data.email }}</a>){% endif %}{% endif %}</header>
|
||||
<div class="texte">{{ step.data.message|safe }}</div>
|
||||
{% if step.data.comments %}
|
||||
<div><strong>Commentaire :</strong> {{ step.data.comments }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.media }}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Commenter">
|
||||
</form>
|
||||
|
||||
{% include "agenda_culturel/event-info-inc.html" with allbutdates=1 %}
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
|
||||
{% cache cache_timeout event_aside user.is_authenticated event %}
|
||||
<aside>
|
||||
{% with event.get_concurrent_events as concurrent_events %}
|
||||
{% if concurrent_events %}
|
||||
@ -51,39 +90,7 @@
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<article>
|
||||
{% with event.get_nb_events_same_dates as nb_events_same_dates %}
|
||||
{% with nb_events_same_dates|length as c_dates %}
|
||||
<header>
|
||||
<h2>Voir aussi</h2>
|
||||
{% if c_dates != 1 %}
|
||||
<p class="remarque">
|
||||
Retrouvez ci-dessous tous les événements
|
||||
{% if event.is_single_day %}
|
||||
à la même date
|
||||
{% else %}
|
||||
aux mêmes dates
|
||||
{% endif %}
|
||||
que l'événement affiché.
|
||||
</p>
|
||||
{% endif %}
|
||||
</header>
|
||||
<nav>
|
||||
{% if c_dates == 1 %}
|
||||
<a role="button" href="{% url 'day_view' nb_events_same_dates.0.1.year nb_events_same_dates.0.1.month nb_events_same_dates.0.1.day %}">Toute la journée</a>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for nbevents_date in nb_events_same_dates %}
|
||||
<li>
|
||||
<a href="{% url 'day_view' nbevents_date.1.year nbevents_date.1.month nbevents_date.1.day %}">{{ nbevents_date.0 }} événement{{ nbevents_date.0 | pluralize }} le {{ nbevents_date.1 }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</article>
|
||||
|
||||
{% if event.other_versions and not event.other_versions.fixed %}
|
||||
{% with poss_dup=event.get_other_versions|only_allowed:user.is_authenticated %}
|
||||
{% if poss_dup|length > 0 %}
|
||||
@ -127,8 +134,10 @@
|
||||
|
||||
|
||||
</aside>
|
||||
|
||||
{% endcache %}
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
@ -84,7 +84,7 @@
|
||||
</script>
|
||||
{% endif %}
|
||||
<header{% if day.is_today %} id="today"{% endif %}>
|
||||
<h3><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h3}>
|
||||
<h3><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">{{ day.date | date:"l j" }}</a></h3}>
|
||||
</header>
|
||||
{% if day.events %}
|
||||
<ul>
|
||||
@ -121,7 +121,8 @@
|
||||
</article>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
<ul>
|
||||
<li class="detail-link"><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">voir en détail {% picto_from_name "chevrons-right" %}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</article>
|
||||
|
@ -40,7 +40,7 @@
|
||||
<li><strong>Valeurs par défaut :</strong>
|
||||
<ul>
|
||||
<li><strong>Publié :</strong> {{ object.defaultPublished|yesno:"Oui,Non" }}</li>
|
||||
{% if object.defaultLocation %}<li><strong>Localisation :</strong> {{ object.defaultLocation }}</li>{% endif %}
|
||||
{% if object.defaultLocation %}<li><strong>Localisation{% if object.forceLocation %} (forcée){% endif %} :</strong> {{ object.defaultLocation }}</li>{% endif %}
|
||||
<li><strong>Catégorie :</strong> {{ object.defaultCategory }}</li>
|
||||
{% if object.defaultOrganiser %}<li><strong>Organisateur :</strong> <a href="{{ object.defaultOrganiser.get_absolute_url }}">{{ object.defaultOrganiser }}</a></li>{% endif %}
|
||||
<li><strong>Étiquettes :</strong>
|
||||
|
@ -95,6 +95,9 @@
|
||||
<h3>{{ ti.short_name }} <a class="badge simple" href="#{{ ti.id }}" data-tooltip="Aller à {{ ti.name }}">{{ ti.events|length }} {% picto_from_name "chevrons-down" %}</a></h3>
|
||||
<ul>
|
||||
{% for event in ti.events %}
|
||||
{% if event.is_first_after_now %}
|
||||
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
|
||||
{% endif %}
|
||||
<li>{{ event.category | circle_cat:event.has_recurrences }}
|
||||
{% if event.start_time %}
|
||||
{{ event.start_time }}
|
||||
@ -102,6 +105,9 @@
|
||||
{{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title|no_emoji }}</a> {{ event|tw_badge }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if forloop.last and cd.is_today_after_events %}
|
||||
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
@ -37,7 +37,7 @@
|
||||
<div>
|
||||
{% if calendar.firstdate|shift_day:-1|not_before_first %}
|
||||
{% if calendar.lastdate|not_after_last %}
|
||||
<a role="button" href="{% url 'week_view' calendar.previous_week.year calendar.previous_week|week %}?{{ filter.get_url }}">
|
||||
<a role="button" href="{% url 'week_view' calendar.previous_week|weekyear calendar.previous_week|week %}?{{ filter.get_url }}">
|
||||
{% picto_from_name "chevron-left" %} précédente</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@ -45,7 +45,7 @@
|
||||
{% if calendar.lastdate|shift_day:+1|not_after_last %}
|
||||
{% if calendar.lastdate|not_before_first %}
|
||||
<div class="right">
|
||||
<a role="button" href="{% url 'week_view' calendar.next_week.year calendar.next_week|week %}?{{ filter.get_url }}">suivante
|
||||
<a role="button" href="{% url 'week_view' calendar.next_week|weekyear calendar.next_week|week %}?{{ filter.get_url }}">suivante
|
||||
{% picto_from_name "chevron-right" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -57,7 +57,7 @@
|
||||
<div class="slider-button slider-button-inside button-left hidden">{% picto_from_name "arrow-left" %}</div>
|
||||
{% if calendar.firstdate|shift_day:-1|not_before_first %}
|
||||
{% if calendar.lastdate|not_after_last %}
|
||||
<div class="slider-button slider-button-page button-left hidden"><a href="{% url 'week_view' calendar.previous_week.year calendar.previous_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-left" %}</a></div>
|
||||
<div class="slider-button slider-button-page button-left hidden"><a href="{% url 'week_view' calendar.previous_week|weekyear calendar.previous_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-left" %}</a></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@ -80,11 +80,14 @@
|
||||
</script>
|
||||
{% endif %}
|
||||
<header{% if day.is_today %} id="today"{% endif %}>
|
||||
<h2><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h2>
|
||||
<h2><a class="visible-link" href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h2>
|
||||
</header>
|
||||
{% if day.events %}
|
||||
<ul>
|
||||
{% for event in day.events %}
|
||||
{% if event.is_first_after_now %}
|
||||
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
|
||||
{% endif %}
|
||||
<li>{{ event.category | circle_cat:event.has_recurrences }}
|
||||
{% if event.start_day == day.date and event.start_time %}
|
||||
{{ event.start_time }}
|
||||
@ -93,7 +96,12 @@
|
||||
{{ event|tw_badge }}
|
||||
<dialog id="event-{{ event.id }}">
|
||||
<article>
|
||||
<header>
|
||||
{% if event.has_image_url %}
|
||||
<header style="background-image: url({{ event.get_image_url }});" class="h-image">
|
||||
{% else %}
|
||||
<header class="cat-{{ event.category.pk }}">
|
||||
{% endif %}
|
||||
<div class="h-mask">
|
||||
<a href="#event-{{ event.id }}"
|
||||
aria-label="Fermer"
|
||||
class="close"
|
||||
@ -125,6 +133,7 @@
|
||||
{% endif %}
|
||||
{% if event.end_time %} {% if not event.end_day|date|frdate or event.end_day == event.start_day %}jusqu'à{% endif %} {{ event.end_time }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="body-fixed">{{ event.description |linebreaks }}</div>
|
||||
@ -147,6 +156,10 @@
|
||||
</dialog>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if day.is_today_after_events %}
|
||||
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
|
||||
{% endif %}
|
||||
<li class="detail-link"><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">voir en détail {% picto_from_name "chevrons-right" %}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</ul>
|
||||
@ -160,7 +173,7 @@
|
||||
<div class="slider-button slider-button-inside button-right hidden">{% picto_from_name "arrow-right" %}</div>
|
||||
{% if calendar.lastdate|shift_day:+1|not_after_last %}
|
||||
{% if calendar.lastdate|not_before_first %}
|
||||
<div class="slider-button slider-button-page button-right hidden"><a href="{% url 'week_view' calendar.next_week.year calendar.next_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-right" %}</a></div>
|
||||
<div class="slider-button slider-button-page button-right hidden"><a href="{% url 'week_view' calendar.next_week|weekyear calendar.next_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-right" %}</a></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
@ -1,5 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
{% load event_extra %}
|
||||
{% load cache %}
|
||||
{% load messages_extra %}
|
||||
{% load utils_extra %}
|
||||
{% load duplicated_extra %}
|
||||
{% load rimports_extra %}
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@ -9,7 +15,7 @@
|
||||
{% load static %}
|
||||
<meta property="og:title" content="Pommes de lune — {% block og_title %}{% endblock %}" />
|
||||
<meta property="og:description" content="{% block og_description %}Événements culturels à Clermont-Ferrand et aux environs{% endblock %}" />
|
||||
<meta property="og:image" content="{% block og_image %}{% static 'images/capture.png' %}{% endblock %}" />
|
||||
<meta property="og:image" content="{% block og_image %}https://{{ request.get_host }}{% get_media_prefix %}screenshot.png{% endblock %}" />
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}" />
|
||||
|
||||
{% if debug %}
|
||||
@ -27,12 +33,7 @@
|
||||
{% block entete_header %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
{% load event_extra %}
|
||||
{% load cache %}
|
||||
{% load contactmessages_extra %}
|
||||
{% load utils_extra %}
|
||||
{% load duplicated_extra %}
|
||||
{% load rimports_extra %}
|
||||
|
||||
<body class="{% block body-class %}contenu{% endblock %}">
|
||||
<div id="boutons-fixes">
|
||||
<ul>
|
||||
@ -50,7 +51,7 @@
|
||||
{% if user.is_authenticated %}{% block configurer-menu %}<li id="menu-configurer" class="configurer-bouton"><a href="{% url 'administration' %}">Administrer {% picto_from_name "settings" %}</a></li>{% endblock %}{% endif %}
|
||||
{% block ajouter-menu %}<li id="menu-ajouter" class="ajouter-bouton"><a href="{% url 'add_event' %}">Ajouter un événement {% picto_from_name "plus-circle" %}</a></li>{% endblock %}
|
||||
{% block rechercher-menu %}<li id="menu-rechercher" class="rechercher-bouton"><a href="{% url 'event_search' %}">Rechercher {% picto_from_name "search" %}</a></li>{% endblock %}
|
||||
<li><a href="{% url 'a_venir' %}{% block a_venir_parameters %}{% endblock %}">À venir</a></li>
|
||||
<li><a href="{% url 'a_venir' %}{% block a_venir_parameters %}{% endblock %}">Maintenant</a></li>
|
||||
<li><a href="{% url 'cette_semaine' %}{% block cette_semaine_parameters %}{% endblock %}">Cette semaine</a></li>
|
||||
<li><a href="{% url 'ce_mois_ci' %}{% block ce_mois_ci_parameters %}{% endblock %}">Ce mois-ci</a></li>
|
||||
</ul>
|
||||
@ -76,8 +77,8 @@
|
||||
{% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %}
|
||||
{% show_badge_unknown_places "bottom" %}
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.view_contactmessage %}
|
||||
{% show_badge_contactmessages "bottom" %}
|
||||
{% if perms.agenda_culturel.view_message %}
|
||||
{% show_badge_messages "bottom" %}
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
{{ user.username }} @
|
||||
|
@ -22,9 +22,26 @@
|
||||
<article>
|
||||
{% if event %}
|
||||
<p>Création d'un lieu depuis l'événement « {{ event }} » (voir en bas de page le détail de l'événement).</p>
|
||||
<p><strong>Remarque :</strong> les champs ont été pré-remplis à partir de la description sous forme libre et n'est probablement pas parfaite.</p>
|
||||
{% endif %}
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_grid }}
|
||||
<div class="grid form-place">
|
||||
{{ form }}
|
||||
<div class="map-widget">
|
||||
<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div>
|
||||
<p>Cliquez pour ajuster la position GPS</p>
|
||||
<input type="checkbox" role="switch" id="lock_position">Verrouiller la position</input>
|
||||
<script>
|
||||
document.getElementById("lock_position").onclick = function() {
|
||||
const field = document.getElementById("id_location");
|
||||
if (this.checked)
|
||||
field.setAttribute("readonly", true);
|
||||
else
|
||||
field.removeAttribute("readonly");
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid buttons">
|
||||
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
|
||||
<input type="submit" value="Envoyer">
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% load event_extra %}
|
||||
{% load contactmessages_extra %}
|
||||
{% load messages_extra %}
|
||||
{% load duplicated_extra %}
|
||||
{% load utils_extra %}
|
||||
<aside id="sidebar">
|
||||
@ -56,11 +56,11 @@
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.view_contactmessage %}
|
||||
{% if perms.agenda_culturel.view_message %}
|
||||
<h3>Messages</h3>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
|
||||
<li><a {% if current == "messages" %}class="selected" {% endif %}href="{% url 'messages' %}">Messages de contact</a>{% show_badge_messages "left" %}</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
@ -68,6 +68,7 @@
|
||||
<h3>Configuration interne</h3>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{% url 'clear_cache' %}">Vider le cache</a></li>
|
||||
<li><a href="{% url 'admin:index' %}">Administration de django</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
@ -6,7 +6,11 @@
|
||||
|
||||
<article id="event-{{ event.pk}}" class="single-event {% if not event.image and not event.local_image %}no-image{% endif %}">
|
||||
<header class="head">
|
||||
{% if day != 0 %}
|
||||
{% if day == 0 %}
|
||||
<div class="small-ephemeride">
|
||||
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if event|can_show_start_time:day %}
|
||||
{% if event.start_time %}
|
||||
<article class='ephemeris-hour'>
|
||||
@ -22,7 +26,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="header-complement">
|
||||
{{ event.category | small_cat_recurrent:event.has_recurrences }}
|
||||
|
||||
{% if event.location or event.exact_location %}<hgroup>{% endif %}
|
||||
@ -50,11 +54,7 @@
|
||||
</hgroup>
|
||||
{% endif %}
|
||||
|
||||
{% if day == 0 %}
|
||||
<div class="small-ephemeride">
|
||||
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if event|need_complete_display:True %}<p>
|
||||
{% picto_from_name "calendar" %}
|
||||
@ -86,12 +86,13 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons" style="clear: both">
|
||||
{% if perms.agenda_culturel.change_event %}
|
||||
{% include "agenda_culturel/edit-buttons-inc.html" with event=event %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="description">
|
||||
|
@ -22,16 +22,17 @@
|
||||
{% endif %}
|
||||
{% if event.end_time %} {% if not event.end_day|date|frdate or event.end_day == event.start_day %}jusqu'à{% endif %} {{ event.end_time }}{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% picto_from_name "map-pin" %}
|
||||
|
||||
|
||||
{% if event.exact_location %}
|
||||
<a href="{{ event.exact_location.get_absolute_url }}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a>
|
||||
<p>{% picto_from_name "map-pin" %}
|
||||
<a href="{{ event.exact_location.get_absolute_url }}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a></p>
|
||||
{% else %}
|
||||
{% if perms.agenda_culturel.change_event and perms.agenda_culturel.change_place %}
|
||||
<a href="{% url 'add_place_to_event' event.pk %}" class="missing-data">{{ event.location }}</a>
|
||||
<p>{% picto_from_name "map-pin" %}
|
||||
<a href="{% url 'add_place_to_event' event.pk %}" class="missing-data">{% if event.location %}{{ event.location }}{% else %}sans lieu{% endif %}</a></p>
|
||||
{% else %}
|
||||
{{ event.location }}
|
||||
{% if event.location %}<p>{% picto_from_name "map-pin" %} {{ event.location }}</p>{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
@ -75,10 +76,10 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.change_contactmessage %}
|
||||
{% if event.contactmessage_set.all.count > 0 %}
|
||||
<p class="remarque">Cet événement a fait l'objet {% if event.contactmessage_set.all.count == 1 %}d'un signalement{% else %}de signalements{% endif %}
|
||||
{% for cm in event.contactmessage_set.all %}
|
||||
{% if perms.agenda_culturel.change_message %}
|
||||
{% if event.message_set.all.count > 0 %}
|
||||
<p class="remarque">Cet événement a fait l'objet {% if event.message_set.all.count == 1 %}d'un signalement{% else %}de signalements{% endif %}
|
||||
{% for cm in event.message_set.all %}
|
||||
<a href="{{ cm.get_absolute_url }}">le {{ cm.date.date }} à {{ cm.date.time }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
@ -133,9 +134,24 @@
|
||||
{% include "agenda_culturel/event-date-info-inc.html" %}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<a href="{% url 'export_event_ical' event.start_day.year event.start_day.month event.start_day.day event.id %}" role="button">Exporter ical {% picto_from_name "calendar" %}</a>
|
||||
{% if perms.agenda_culturel.change_event and not noedit %}
|
||||
{% include "agenda_culturel/edit-buttons-inc.html" with event=event with_clone=1 %}
|
||||
{% if onlyedit %}
|
||||
{% if event.pure_import %}
|
||||
{% with event.get_local_version as local %}
|
||||
{% if local %}
|
||||
<a href="{{ local.get_absolute_url }}" role="button">voir la version locale {% picto_from_name "eye" %}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'clone_edit' event.id %}" role="button">créer une copie locale {% picto_from_name "plus-circle" %}</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<a href="{% url 'edit_event' event.id %}" role="button">modifier {% picto_from_name "edit-3" %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<a href="{% url 'export_event_ical' event.start_day.year event.start_day.month event.start_day.day event.id %}" role="button">Exporter ical {% picto_from_name "calendar" %}</a>
|
||||
{% if perms.agenda_culturel.change_event and not noedit %}
|
||||
{% include "agenda_culturel/edit-buttons-inc.html" with event=event with_clone=1 %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -86,12 +86,6 @@ def css_categories():
|
||||
)
|
||||
result += "}"
|
||||
|
||||
result += "*:hover>." + c["css_class"] + " {"
|
||||
result += background_color_adjust_color(
|
||||
adjust_lightness_saturation(c["color"], 0.02, 1.0)
|
||||
)
|
||||
result += "}"
|
||||
|
||||
result += "." + c["css_class"] + ".circ-cat, "
|
||||
result += "form ." + c["css_class"] + ", "
|
||||
result += ".selected ." + c["css_class"] + " {"
|
||||
|
@ -179,4 +179,8 @@ def tw_badge(event):
|
||||
if event.tags and len([t for t in event.tags if t.startswith("TW:")]) > 0:
|
||||
return mark_safe('<span class="badge tw-badge">TW</span>')
|
||||
else:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
@register.filter
|
||||
def get_image_uri(event, request):
|
||||
return event.get_image_url(request)
|
@ -4,7 +4,7 @@ from django.urls import reverse_lazy
|
||||
from django.template.defaultfilters import pluralize
|
||||
|
||||
|
||||
from agenda_culturel.models import ContactMessage
|
||||
from agenda_culturel.models import Message
|
||||
|
||||
from .utils_extra import picto_from_name
|
||||
|
||||
@ -12,15 +12,15 @@ register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def show_badge_contactmessages(placement="top"):
|
||||
nb_open = ContactMessage.nb_open_contactmessages()
|
||||
def show_badge_messages(placement="top"):
|
||||
nb_open = Message.nb_open_messages()
|
||||
if nb_open != 0:
|
||||
return mark_safe(
|
||||
'<a href="'
|
||||
+ reverse_lazy("contactmessages")
|
||||
+ reverse_lazy("messages")
|
||||
+ '?closed=False" class="badge" data-placement="'
|
||||
+ placement
|
||||
+ '"data-tooltip="'
|
||||
+ '" data-tooltip="'
|
||||
+ str(nb_open)
|
||||
+ " message"
|
||||
+ pluralize(nb_open)
|
@ -29,6 +29,9 @@ def add_de(txt):
|
||||
def week(d):
|
||||
return d.isocalendar()[1]
|
||||
|
||||
@register.filter
|
||||
def weekyear(d):
|
||||
return d.isocalendar()[0]
|
||||
|
||||
@register.filter
|
||||
def not_before_first(d):
|
||||
|
@ -4,10 +4,32 @@ from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.urls import path, include, re_path
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.contrib.sitemaps import GenericSitemap
|
||||
from .sitemaps import StaticViewSitemap
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
from .views import *
|
||||
|
||||
event_dict = {
|
||||
"queryset": Event.objects.all(),
|
||||
"date_field": "modified_date",
|
||||
}
|
||||
place_dict = {
|
||||
"queryset": Place.objects.all(),
|
||||
}
|
||||
organisation_dict = {
|
||||
"queryset": Organisation.objects.all(),
|
||||
}
|
||||
|
||||
|
||||
sitemaps = {
|
||||
"static": StaticViewSitemap,
|
||||
"events": GenericSitemap(event_dict, priority=0.7),
|
||||
"places": GenericSitemap(place_dict, priority=0.6),
|
||||
"organisations": GenericSitemap(organisation_dict, priority=0.2),
|
||||
}
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path("semaine/<int:year>/<int:week>/", week_view, name="week_view"),
|
||||
@ -35,12 +57,13 @@ urlpatterns = [
|
||||
),
|
||||
path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
|
||||
path("event/<int:pk>/moderate", EventModerateView.as_view(), name="moderate_event"),
|
||||
path("event/<int:pk>/moderate-next", EventModerateView.as_view(), name="moderate_event_next"),
|
||||
path("event/<int:pk>/moderate-next/error", error_next_event, name="error_next_event"),
|
||||
path("event/<int:pk>/moderate/after/<int:pred>", EventModerateView.as_view(), name="moderate_event_step"),
|
||||
path("event/<int:pk>/moderate-next", moderate_event_next, name="moderate_event_next"),
|
||||
path("moderate", EventModerateView.as_view(), name="moderate"),
|
||||
path("moderate/<int:y>/<int:m>/<int:d>", moderate_from_date, name="moderate_from_date"),
|
||||
path("event/<int:pk>/simple-clone/edit", EventUpdateView.as_view(), name="simple_clone_edit"),
|
||||
path("event/<int:pk>/clone/edit", EventUpdateView.as_view(), name="clone_edit"),
|
||||
path("event/<int:pk>/message", ContactMessageCreateView.as_view(), name="message_for_event"),
|
||||
path("event/<int:pk>/message", MessageCreateView.as_view(), name="message_for_event"),
|
||||
path("event/<int:pk>/update-from-source", update_from_source, name="update_from_source"),
|
||||
path(
|
||||
"event/<int:pk>/change-status/<status>",
|
||||
@ -75,18 +98,18 @@ urlpatterns = [
|
||||
path("mentions-legales", mentions_legales, name="mentions_legales"),
|
||||
path("a-propos", about, name="about"),
|
||||
path("merci", thank_you, name="thank_you"),
|
||||
path("contact", ContactMessageCreateView.as_view(), name="contact"),
|
||||
path("contactmessages", contactmessages, name="contactmessages"),
|
||||
path("contactmessages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
|
||||
path("contact", MessageCreateView.as_view(), name="contact"),
|
||||
path("messages", view_messages, name="messages"),
|
||||
path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
|
||||
path(
|
||||
"contactmessage/<int:pk>",
|
||||
ContactMessageUpdateView.as_view(),
|
||||
name="contactmessage",
|
||||
"message/<int:pk>",
|
||||
MessageUpdateView.as_view(),
|
||||
name="message",
|
||||
),
|
||||
path(
|
||||
"contactmessage/<int:pk>/delete",
|
||||
ContactMessageDeleteView.as_view(),
|
||||
name="delete_contactmessage",
|
||||
"message/<int:pk>/delete",
|
||||
MessageDeleteView.as_view(),
|
||||
name="delete_message",
|
||||
),
|
||||
path("imports/", imports, name="imports"),
|
||||
path("imports/add", add_import, name="add_import"),
|
||||
@ -134,7 +157,8 @@ urlpatterns = [
|
||||
path("500/", internal_server_error, name="internal_server_error"),
|
||||
|
||||
path("organisme/<int:pk>/past", OrganisationDetailViewPast.as_view(), name="view_organisation_past"),
|
||||
path("organisme/<int:pk>", OrganisationDetailView.as_view(), name="view_organisation"),
|
||||
path("organisme/<int:pk>", OrganisationDetailView.as_view(), name="view_organisation_shortname"),
|
||||
path("organisme/<int:pk>-<extra>", OrganisationDetailView.as_view(), name="view_organisation"),
|
||||
path("organisme/<int:pk>-<extra>/past", OrganisationDetailViewPast.as_view(), name="view_organisation_past_fullname"),
|
||||
path("organisme/<int:pk>-<extra>", OrganisationDetailView.as_view(), name="view_organisation_fullname"),
|
||||
path("organisme/<int:pk>/edit", OrganisationUpdateView.as_view(), name="edit_organisation"),
|
||||
@ -178,6 +202,14 @@ urlpatterns = [
|
||||
re_path(r'^robots\.txt', include('robots.urls')),
|
||||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
path("ckeditor5/", include('django_ckeditor_5.urls')),
|
||||
path(
|
||||
"sitemap.xml",
|
||||
cache_page(86400)(sitemap),
|
||||
{"sitemaps": sitemaps},
|
||||
name="django.contrib.sitemaps.views.sitemap",
|
||||
),
|
||||
path("cache/clear", clear_cache, name="clear_cache"),
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
114
src/agenda_culturel/utils.py
Normal file
114
src/agenda_culturel/utils.py
Normal file
@ -0,0 +1,114 @@
|
||||
from agenda_culturel.models import ReferenceLocation
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
class PlaceGuesser:
|
||||
|
||||
def __init__(self):
|
||||
self.__citynames = list(ReferenceLocation.objects.values_list("name__lower__unaccent", "name")) + [("clermont-fd", "Clermont-Ferrand"), ("aurillac", "Aurillac"), ("montlucon", "Montluçon"), ("montferrand", "Clermont-Ferrand")]
|
||||
self.__citynames = [(x[0].replace("-", " "), x[1]) for x in self.__citynames]
|
||||
|
||||
def __remove_accents(self, input_str):
|
||||
if input_str is None:
|
||||
return None
|
||||
nfkd_form = unicodedata.normalize("NFKD", input_str)
|
||||
return "".join([c for c in nfkd_form if not unicodedata.combining(c)])
|
||||
|
||||
|
||||
def __guess_is_address(self, part):
|
||||
toponyms = ["bd", "rue", "avenue", "place", "boulevard", "allee", ]
|
||||
part = part.strip()
|
||||
if re.match(r'^[0-9]', part):
|
||||
return True
|
||||
|
||||
elems = part.split(" ")
|
||||
return any([self.__remove_accents(e.lower()) in toponyms for e in elems])
|
||||
|
||||
|
||||
def __clean_address(self, addr):
|
||||
toponyms = ["bd", "rue", "avenue", "place", "boulevard", "allée", "bis", "ter", "ZI"]
|
||||
for t in toponyms:
|
||||
addr = re.sub(" " + t + " ", " " + t + " ", addr, flags=re.IGNORECASE)
|
||||
return addr
|
||||
|
||||
def __guess_city_name(self, part):
|
||||
part = part.strip().replace(" - ", "-")
|
||||
if len(part) == 0:
|
||||
return None
|
||||
part = self.__remove_accents(part.lower()).replace("-", " ")
|
||||
match = [x[1] for x in self.__citynames if x[0] == part]
|
||||
if len(match) > 0:
|
||||
return match[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def __guess_city_name_postcode(self, part):
|
||||
with_pc = re.search(r'^(.*)(([0-9][ ]*){5})(.*)$', part)
|
||||
if with_pc:
|
||||
p1 = self.__guess_city_name(with_pc.group(1).strip())
|
||||
postcode = with_pc.group(2).replace(" ", "")
|
||||
p2 = self.__guess_city_name(with_pc.group(4).strip())
|
||||
return postcode, p2, p1
|
||||
else:
|
||||
return None, self.__guess_city_name(part), None
|
||||
|
||||
def __guess_name_address(self, part):
|
||||
with_num = re.search(r'^(([^0-9])+)([0-9]+)(.*)', part)
|
||||
if with_num:
|
||||
name = with_num.group(1)
|
||||
return name, part[len(name):]
|
||||
else:
|
||||
return "", part
|
||||
|
||||
def guess_address_elements(self, alias):
|
||||
parts = re.split(r'[,/à]', alias)
|
||||
parts = [p1 for p1 in [p.strip() for p in parts] if p1 != "" and p1.lower() != "france"]
|
||||
|
||||
name = ""
|
||||
address = ""
|
||||
postcode = ""
|
||||
city = ""
|
||||
possible_city = ""
|
||||
|
||||
oparts = []
|
||||
for part in parts:
|
||||
p, c, possible_c = self.__guess_city_name_postcode(part)
|
||||
if not possible_c is None:
|
||||
possible_city = possible_c
|
||||
if not c is None and city == "":
|
||||
city = c
|
||||
if not p is None and postcode == "":
|
||||
postcode = p
|
||||
if p is None and c is None:
|
||||
oparts.append(part)
|
||||
|
||||
if city == "" and possible_city != "":
|
||||
city = possible_city
|
||||
else:
|
||||
if len(oparts) == 0 and not possible_city != "":
|
||||
oparts = [possible_city]
|
||||
|
||||
if city == "":
|
||||
alias_simple = self.__remove_accents(alias.lower()).replace("-", " ")
|
||||
mc = [x[1] for x in self.__citynames if alias_simple.endswith(" " + x[0])]
|
||||
if len(mc) == 1:
|
||||
city = mc[0]
|
||||
|
||||
|
||||
|
||||
|
||||
if len(oparts) > 0:
|
||||
if not self.__guess_is_address(oparts[0]):
|
||||
name = oparts[0]
|
||||
address = ", ".join(oparts[1:])
|
||||
else:
|
||||
name, address = self.__guess_name_address(", ".join(oparts))
|
||||
|
||||
address = self.__clean_address(address)
|
||||
|
||||
if name == "" and possible_city != "" and possible_city != city:
|
||||
name = possible_city
|
||||
|
||||
return name, address, postcode, city
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views.generic import ListView, DetailView
|
||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView, ModelFormMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
LoginRequiredMixin,
|
||||
UserPassesTestMixin,
|
||||
@ -10,6 +10,12 @@ from django import forms
|
||||
from django.http import Http404
|
||||
from django.contrib.postgres.search import SearchQuery, SearchHeadline
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.decorators import method_decorator
|
||||
from honeypot.decorators import check_honeypot
|
||||
from .utils import PlaceGuesser
|
||||
import hashlib
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.contrib.gis.measure import D
|
||||
@ -37,13 +43,14 @@ from .forms import (
|
||||
EventModerateForm,
|
||||
TagForm,
|
||||
TagRenameForm,
|
||||
ContactMessageForm,
|
||||
MessageForm,
|
||||
MessageEventForm,
|
||||
)
|
||||
|
||||
from .filters import (
|
||||
EventFilter,
|
||||
EventFilterAdmin,
|
||||
ContactMessagesFilterAdmin,
|
||||
MessagesFilterAdmin,
|
||||
SimpleSearchEventFilter,
|
||||
SearchEventFilter,
|
||||
DuplicatedEventsFilter,
|
||||
@ -55,7 +62,7 @@ from .models import (
|
||||
Category,
|
||||
Tag,
|
||||
StaticContent,
|
||||
ContactMessage,
|
||||
Message,
|
||||
BatchImportation,
|
||||
DuplicatedEvents,
|
||||
RecurrentImport,
|
||||
@ -69,7 +76,7 @@ from django.utils import timezone
|
||||
from django.utils.html import escape
|
||||
from datetime import date, timedelta
|
||||
from django.utils.timezone import datetime
|
||||
from django.db.models import Q, Subquery, OuterRef, Count, F, Func
|
||||
from django.db.models import Q, Subquery, OuterRef, Count, F, Func, BooleanField, ExpressionWrapper
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -293,7 +300,7 @@ def update_from_source(request, pk):
|
||||
if url is None:
|
||||
messages.warning(request, _("The event cannot be updated because the import process is not available for the referenced sources."))
|
||||
else:
|
||||
import_events_from_url.delay(url, None, True)
|
||||
import_events_from_url.delay(url, None, None, True, user_id=request.user.pk if request.user else None)
|
||||
messages.success(request, _("The event update has been queued and will be completed shortly."))
|
||||
|
||||
return HttpResponseRedirect(event.get_absolute_url())
|
||||
@ -313,6 +320,10 @@ class EventUpdateView(
|
||||
kwargs["is_simple_cloning"] = self.is_simple_cloning
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.set_processing_user(self.request.user)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_initial(self):
|
||||
self.is_cloning = "clone" in self.request.path.split('/')
|
||||
self.is_simple_cloning = "simple-clone" in self.request.path.split('/')
|
||||
@ -328,6 +339,7 @@ class EventUpdateView(
|
||||
obj.save()
|
||||
result["other_versions"] = obj.other_versions
|
||||
result["status"] = Event.STATUS.PUBLISHED
|
||||
result["cloning"] = True
|
||||
|
||||
if self.is_simple_cloning:
|
||||
result["other_versions"] = None
|
||||
@ -349,21 +361,27 @@ class EventModerateView(
|
||||
permission_required = "agenda_culturel.change_event"
|
||||
template_name = "agenda_culturel/event_form_moderate.html"
|
||||
form_class = EventModerateForm
|
||||
success_message = _("The event has been successfully moderated.")
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return mark_safe(_('The event <a href="{}">{}</a> has been moderated with success.').format(self.object.get_absolute_url(), self.object.title))
|
||||
|
||||
|
||||
def is_moderate_next(self):
|
||||
return "moderate-next" in self.request.path.split('/')
|
||||
return "after" in self.request.path.split('/')
|
||||
|
||||
def is_starting_moderation(self):
|
||||
return not "pk" in self.kwargs
|
||||
|
||||
def is_moderation_from_date(self):
|
||||
return "m" in self.kwargs and "y" in self.kwargs and "d" in self.kwargs
|
||||
|
||||
def get_next_event(self, start_day, start_time):
|
||||
def get_next_event(start_day, start_time):
|
||||
# select non moderated events
|
||||
qs = Event.objects.filter(moderated_date__isnull=True)
|
||||
|
||||
# select events after the current one
|
||||
if start_time:
|
||||
qs = qs.filter(Q(start_day__gt=start_day)|(Q(start_day=start_day) & (Q(start_time__isnull=True)|Q(start_time__gte=start_time))))
|
||||
qs = qs.filter(Q(start_day__gt=start_day)|(Q(start_day=start_day) & (Q(start_time__isnull=True)|Q(start_time__gt=start_time))))
|
||||
else:
|
||||
qs = qs.filter(start_day__gte=start_day)
|
||||
|
||||
@ -384,19 +402,15 @@ class EventModerateView(
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.is_moderate_next():
|
||||
context['pred'] = self.kwargs["pk"]
|
||||
context['pred'] = self.kwargs["pred"]
|
||||
return context
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
if self.is_starting_moderation():
|
||||
now = datetime.now()
|
||||
return self.get_next_event(now.date(), now.time())
|
||||
return EventModerateView.get_next_event(now.date(), now.time())
|
||||
else:
|
||||
result = super().get_object(queryset)
|
||||
if self.is_moderate_next():
|
||||
return self.get_next_event(result.start_day, result.start_time)
|
||||
else:
|
||||
return result
|
||||
return super().get_object(queryset)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
@ -408,15 +422,12 @@ class EventModerateView(
|
||||
def form_valid(self, form):
|
||||
form.instance.set_no_modification_date_changed()
|
||||
form.instance.set_in_moderation_process()
|
||||
form.instance.set_processing_user(self.request.user)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
if 'save_and_next' in self.request.POST:
|
||||
return reverse_lazy("moderate_event_next", args=[self.object.pk])
|
||||
elif 'save_and_create_local' in self.request.POST:
|
||||
return reverse_lazy("clone_edit", args=[self.object.pk])
|
||||
elif 'save_and_edit' in self.request.POST:
|
||||
return reverse_lazy("edit_event", args=[self.object.pk])
|
||||
elif 'save_and_edit_local' in self.request.POST:
|
||||
return reverse_lazy("edit_event", args=[self.object.get_local_version().pk])
|
||||
else:
|
||||
@ -435,6 +446,30 @@ def error_next_event(request, pk):
|
||||
{"pk": pk, "object": obj},
|
||||
)
|
||||
|
||||
@login_required(login_url="/accounts/login/")
|
||||
@permission_required("agenda_culturel.change_event")
|
||||
def moderate_event_next(request, pk):
|
||||
# current event
|
||||
obj = Event.objects.filter(pk=pk).first()
|
||||
start_day = obj.start_day
|
||||
start_time = obj.start_time
|
||||
|
||||
next_obj = EventModerateView.get_next_event(start_day, start_time)
|
||||
if next_obj is None:
|
||||
return render(
|
||||
request,
|
||||
"agenda_culturel/event_next_error_message.html",
|
||||
{"pk": pk, "object": obj},
|
||||
)
|
||||
else:
|
||||
return HttpResponseRedirect(reverse_lazy("moderate_event_step", args=[next_obj.pk, obj.pk]))
|
||||
|
||||
@login_required(login_url="/accounts/login/")
|
||||
@permission_required("agenda_culturel.change_event")
|
||||
def moderate_from_date(request, y, m, d):
|
||||
d = date(y, m, d)
|
||||
obj = EventModerateView.get_next_event(d, None)
|
||||
return HttpResponseRedirect(reverse_lazy("moderate_event", args=[obj.pk]))
|
||||
|
||||
|
||||
class EventDeleteView(
|
||||
@ -446,9 +481,12 @@ class EventDeleteView(
|
||||
success_message = _("The event has been successfully deleted.")
|
||||
|
||||
|
||||
class EventDetailView(UserPassesTestMixin, DetailView):
|
||||
class EventDetailView(UserPassesTestMixin, DetailView, ModelFormMixin):
|
||||
model = Event
|
||||
form_class = MessageEventForm
|
||||
template_name = "agenda_culturel/page-event.html"
|
||||
queryset = Event.objects.select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative").prefetch_related("message_set")
|
||||
|
||||
|
||||
def test_func(self):
|
||||
return (
|
||||
@ -458,6 +496,8 @@ class EventDetailView(UserPassesTestMixin, DetailView):
|
||||
|
||||
def get_object(self):
|
||||
o = super().get_object()
|
||||
logger.warning(">>>> details")
|
||||
o.download_missing_image()
|
||||
y = self.kwargs["year"]
|
||||
m = self.kwargs["month"]
|
||||
d = self.kwargs["day"]
|
||||
@ -465,6 +505,30 @@ class EventDetailView(UserPassesTestMixin, DetailView):
|
||||
obj.set_current_date(date(y, m, d))
|
||||
return obj
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_object().get_absolute_url() + "#chronology"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseForbidden()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
message = form.save(commit=False)
|
||||
message.user = self.request.user
|
||||
message.related_event = self.get_object()
|
||||
message.subject = _("Comment")
|
||||
message.spam = False
|
||||
message.closed = True
|
||||
message.save()
|
||||
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
|
||||
@login_required(login_url="/accounts/login/")
|
||||
@ -518,31 +582,16 @@ class EventCreateView(SuccessMessageMixin, CreateView):
|
||||
|
||||
if form.cleaned_data['simple_cloning']:
|
||||
form.instance.set_skip_duplicate_check()
|
||||
|
||||
if form.cleaned_data['cloning']:
|
||||
form.instance.set_in_moderation_process()
|
||||
|
||||
form.instance.import_sources = None
|
||||
form.instance.set_processing_user(self.request.user)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
def import_from_details(request):
|
||||
form = EventForm(request.POST, is_authenticated=request.user.is_authenticated)
|
||||
if form.is_valid():
|
||||
new_event = form.save()
|
||||
if request.user.is_authenticated:
|
||||
messages.success(request, _("The event is saved."))
|
||||
return HttpResponseRedirect(new_event.get_absolute_url())
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_(
|
||||
"The event has been submitted and will be published as soon as it has been validated by the moderation team."
|
||||
),
|
||||
)
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
else:
|
||||
return render(
|
||||
request, "agenda_culturel/event_form.html", context={"form": form}
|
||||
)
|
||||
|
||||
# A class to evaluate the URL according to the existing events and the authentification
|
||||
# level of the user
|
||||
@ -640,7 +689,7 @@ def import_from_urls(request):
|
||||
request,
|
||||
_('Integrating {} url(s) into our import process.').format(len(ucat))
|
||||
)
|
||||
import_events_from_urls.delay(ucat)
|
||||
import_events_from_urls.delay(ucat, user_id=request.user.pk if request.user else None)
|
||||
return HttpResponseRedirect(reverse("thank_you"))
|
||||
else:
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
@ -690,7 +739,7 @@ def import_from_url(request):
|
||||
request,
|
||||
_('Integrating {} into our import process.').format(uc.url)
|
||||
)
|
||||
import_events_from_url.delay(uc.url, uc.cat, uc.tags)
|
||||
import_events_from_url.delay(uc.url, uc.cat, uc.tags, user_id=request.user.pk if request.user else None)
|
||||
return HttpResponseRedirect(reverse("thank_you"))
|
||||
|
||||
|
||||
@ -708,7 +757,7 @@ def export_event_ical(request, year, month, day, pk):
|
||||
events = list()
|
||||
events.append(event)
|
||||
|
||||
cal = Event.export_to_ics(events)
|
||||
cal = Event.export_to_ics(events, request)
|
||||
|
||||
response = HttpResponse(content_type="text/calendar")
|
||||
response.content = cal.to_ical().decode("utf-8").replace("\r\n", "\n")
|
||||
@ -718,14 +767,17 @@ def export_event_ical(request, year, month, day, pk):
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def export_ical(request):
|
||||
now = date.today()
|
||||
|
||||
request = EventFilter.set_default_values(request)
|
||||
filter = EventFilter(request.GET, queryset=get_event_qs(request), request=request)
|
||||
calendar = CalendarList(now + timedelta(days=-7), now + timedelta(days=+60), filter)
|
||||
ical = calendar.export_to_ics()
|
||||
id_cache = hashlib.md5(filter.get_url().encode("utf8")).hexdigest()
|
||||
ical = cache.get(id_cache)
|
||||
if not ical:
|
||||
calendar = CalendarList(now + timedelta(days=-7), now + timedelta(days=+60), filter)
|
||||
ical = calendar.export_to_ics(request)
|
||||
cache.set(id_cache, ical, 3600) # 1 heure
|
||||
|
||||
response = HttpResponse(content_type="text/calendar")
|
||||
response.content = ical.to_ical().decode("utf-8").replace("\r\n", "\n")
|
||||
@ -736,15 +788,19 @@ def export_ical(request):
|
||||
return response
|
||||
|
||||
|
||||
|
||||
class ContactMessageCreateView(SuccessMessageMixin, CreateView):
|
||||
model = ContactMessage
|
||||
template_name = "agenda_culturel/contactmessage_create_form.html"
|
||||
form_class = ContactMessageForm
|
||||
@method_decorator(check_honeypot, name='post')
|
||||
class MessageCreateView(SuccessMessageMixin, CreateView):
|
||||
model = Message
|
||||
template_name = "agenda_culturel/message_create_form.html"
|
||||
form_class = MessageForm
|
||||
|
||||
success_url = reverse_lazy("home")
|
||||
success_message = _("Your message has been sent successfully.")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
if form_class is None:
|
||||
form_class = self.get_form_class()
|
||||
@ -753,39 +809,48 @@ class ContactMessageCreateView(SuccessMessageMixin, CreateView):
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["event"] = self.event
|
||||
if self.request.user.is_authenticated:
|
||||
kwargs["internal"] = True
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.user = self.request.user
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
def get_initial(self):
|
||||
result = super().get_initial()
|
||||
if "pk" in self.kwargs:
|
||||
self.event = get_object_or_404(Event, pk=self.kwargs["pk"])
|
||||
result["related_event"] = self.event
|
||||
result["subject"] = _('Reporting the event {} on {}').format(self.event.title, self.event.start_day)
|
||||
else:
|
||||
result["related_event"] = None
|
||||
return result
|
||||
|
||||
|
||||
|
||||
class ContactMessageDeleteView(SuccessMessageMixin, DeleteView):
|
||||
model = ContactMessage
|
||||
class MessageDeleteView(SuccessMessageMixin, DeleteView):
|
||||
model = Message
|
||||
success_message = _(
|
||||
"The contact message has been successfully deleted."
|
||||
)
|
||||
success_url = reverse_lazy("contactmessages")
|
||||
success_url = reverse_lazy("messages")
|
||||
|
||||
|
||||
class ContactMessageUpdateView(
|
||||
class MessageUpdateView(
|
||||
SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView
|
||||
):
|
||||
model = ContactMessage
|
||||
permission_required = "agenda_culturel.change_contactmessage"
|
||||
template_name = "agenda_culturel/contactmessage_moderation_form.html"
|
||||
model = Message
|
||||
permission_required = "agenda_culturel.change_message"
|
||||
template_name = "agenda_culturel/message_moderation_form.html"
|
||||
fields = ("spam", "closed", "comments")
|
||||
|
||||
success_message = _(
|
||||
"The contact message properties has been successfully modified."
|
||||
)
|
||||
|
||||
success_url = reverse_lazy("contactmessages")
|
||||
success_url = reverse_lazy("messages")
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Return the keyword arguments for instantiating the form."""
|
||||
@ -822,23 +887,27 @@ def activite(request):
|
||||
@login_required(login_url="/accounts/login/")
|
||||
@permission_required("agenda_culturel.view_event")
|
||||
def administration(request):
|
||||
nb_mod_days = 21
|
||||
nb_classes = 4
|
||||
today = date.today()
|
||||
start_time = datetime.now().time()
|
||||
|
||||
# get information about recent modifications
|
||||
days = [date.today()]
|
||||
days = [today]
|
||||
for i in range(0, 2):
|
||||
days.append(days[-1] + timedelta(days=-1))
|
||||
daily_modifications = Event.get_count_modifications([(d, 1) for d in days])
|
||||
|
||||
# get last created events
|
||||
events = Event.objects.all().order_by("-created_date")[:5]
|
||||
events = Event.objects.all().order_by("-created_date").select_related("exact_location", "category")[:5]
|
||||
|
||||
# get last batch imports
|
||||
batch_imports = BatchImportation.objects.all().order_by("-created_date")[:5]
|
||||
batch_imports = BatchImportation.objects.all().select_related("recurrentImport").order_by("-created_date")[:5]
|
||||
|
||||
# get info about batch information
|
||||
newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by(
|
||||
"-created_date"
|
||||
)
|
||||
).select_related("recurrentImport")
|
||||
imported_events = RecurrentImport.objects.annotate(
|
||||
last_run_status=Subquery(newest.values("status")[:1])
|
||||
)
|
||||
@ -854,13 +923,41 @@ def administration(request):
|
||||
.count())
|
||||
nb_all = imported_events.count()
|
||||
|
||||
window_end = today + timedelta(days=nb_mod_days)
|
||||
# get all non moderated events
|
||||
nb_not_moderated = Event.objects.filter(~Q(status=Event.STATUS.TRASH)). \
|
||||
filter(Q(start_day__gte=today)&Q(start_day__lte=window_end)). \
|
||||
filter(
|
||||
Q(other_versions__isnull=True) |
|
||||
Q(other_versions__representative=F('pk')) |
|
||||
Q(other_versions__representative__isnull=True)).values("start_day").\
|
||||
annotate(not_moderated=Count("start_day", filter=Q(moderated_date__isnull=True))). \
|
||||
annotate(nb_events=Count("start_day")). \
|
||||
order_by("start_day").values("not_moderated", "nb_events", "start_day")
|
||||
|
||||
max_not_moderated = max([x["not_moderated"] for x in nb_not_moderated])
|
||||
if max_not_moderated == 0:
|
||||
max_not_moderated = 1
|
||||
nb_not_moderated_dict = dict([(x["start_day"], (x["not_moderated"], x["nb_events"])) for x in nb_not_moderated])
|
||||
# add missing dates
|
||||
date_list = [today + timedelta(days=x) for x in range(0, nb_mod_days)]
|
||||
nb_not_moderated = [{"start_day": d,
|
||||
"is_today": d == today,
|
||||
"nb_events": nb_not_moderated_dict[d][1] if d in nb_not_moderated_dict else 0,
|
||||
"not_moderated": nb_not_moderated_dict[d][0] if d in nb_not_moderated_dict else 0} for d in date_list]
|
||||
nb_not_moderated = [ x | { "note": 0 if x["not_moderated"] == 0 else int((nb_classes - 1) * x["not_moderated"] / max_not_moderated) + 1 } for x in nb_not_moderated]
|
||||
nb_not_moderated = [nb_not_moderated[x:x + 7] for x in range(0, len(nb_not_moderated), 7)]
|
||||
|
||||
|
||||
|
||||
return render(
|
||||
request,
|
||||
"agenda_culturel/administration.html",
|
||||
{"daily_modifications": daily_modifications,
|
||||
"events": events, "batch_imports": batch_imports,
|
||||
"nb_failed": nb_failed, "nb_canceled": nb_canceled,
|
||||
"nb_running": nb_running, "nb_all": nb_all},
|
||||
"nb_running": nb_running, "nb_all": nb_all,
|
||||
"nb_not_moderated": nb_not_moderated},
|
||||
)
|
||||
|
||||
|
||||
@ -889,15 +986,15 @@ def recent(request):
|
||||
|
||||
|
||||
@login_required(login_url="/accounts/login/")
|
||||
@permission_required("agenda_culturel.view_contactmessage")
|
||||
def contactmessages(request):
|
||||
filter = ContactMessagesFilterAdmin(
|
||||
request.GET, queryset=ContactMessage.objects.all().order_by("-date")
|
||||
@permission_required("agenda_culturel.view_message")
|
||||
def view_messages(request):
|
||||
filter = MessagesFilterAdmin(
|
||||
request.GET, queryset=Message.objects.all().order_by("-date")
|
||||
)
|
||||
paginator = PaginatorFilter(filter, 10, request)
|
||||
page = request.GET.get("page")
|
||||
|
||||
nb_spams = ContactMessage.objects.filter(spam=True).count()
|
||||
nb_spams = Message.objects.filter(spam=True).count()
|
||||
|
||||
try:
|
||||
response = paginator.page(page)
|
||||
@ -908,24 +1005,24 @@ def contactmessages(request):
|
||||
|
||||
return render(
|
||||
request,
|
||||
"agenda_culturel/contactmessages.html",
|
||||
"agenda_culturel/messages.html",
|
||||
{"filter": filter, "nb_spams": nb_spams, "paginator_filter": response},
|
||||
)
|
||||
|
||||
@login_required(login_url="/accounts/login/")
|
||||
@permission_required("agenda_culturel.view_contactmessage")
|
||||
@permission_required("agenda_culturel.view_message")
|
||||
def delete_cm_spam(request):
|
||||
|
||||
if request.method == "POST":
|
||||
ContactMessage.objects.filter(spam=True).delete()
|
||||
Message.objects.filter(spam=True).delete()
|
||||
|
||||
messages.success(request, _("Spam has been successfully deleted."))
|
||||
return HttpResponseRedirect(reverse_lazy("contactmessages"))
|
||||
return HttpResponseRedirect(reverse_lazy("messages"))
|
||||
else:
|
||||
nb_msgs = ContactMessage.objects.values('spam').annotate(total=Count('spam'))
|
||||
nb_msgs = Message.objects.values('spam').annotate(total=Count('spam'))
|
||||
nb_total = sum([nb["total"] for nb in nb_msgs])
|
||||
nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]])
|
||||
cancel_url = reverse_lazy("contactmessages")
|
||||
cancel_url = reverse_lazy("messages")
|
||||
return render(
|
||||
request,
|
||||
"agenda_culturel/delete_spams_confirm.html",
|
||||
@ -1481,6 +1578,7 @@ def set_duplicate(request, year, month, day, pk):
|
||||
event.other_versions is None
|
||||
or event.other_versions != e.other_versions
|
||||
)
|
||||
and e.status != Event.STATUS.TRASH
|
||||
]
|
||||
|
||||
form = SelectEventInList(events=others)
|
||||
@ -1856,6 +1954,7 @@ class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateVi
|
||||
|
||||
|
||||
class PlaceFromEventCreateView(PlaceCreateView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["event"] = self.event
|
||||
@ -1866,6 +1965,14 @@ class PlaceFromEventCreateView(PlaceCreateView):
|
||||
self.event = get_object_or_404(Event, pk=self.kwargs["pk"])
|
||||
if self.event.location and "add" in self.request.GET:
|
||||
initial["aliases"] = [self.event.location]
|
||||
guesser = PlaceGuesser()
|
||||
name, address, postcode, city = guesser.guess_address_elements(self.event.location)
|
||||
initial["name"] = name
|
||||
initial["address"] = address
|
||||
initial["postcode"] = postcode
|
||||
initial["city"] = city
|
||||
initial["location"] = ""
|
||||
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -2125,3 +2232,15 @@ def delete_tag(request, t):
|
||||
"agenda_culturel/tag_confirm_delete_by_name.html",
|
||||
{"tag": t, "nb": nb, "nbi": nbi, "cancel_url": cancel_url, "obj": obj},
|
||||
)
|
||||
|
||||
|
||||
def clear_cache(request):
|
||||
if request.method == "POST":
|
||||
cache.clear()
|
||||
messages.success(request, _("Cache successfully cleared."))
|
||||
return HttpResponseRedirect(reverse_lazy("administration"))
|
||||
else:
|
||||
return render(
|
||||
request,
|
||||
"agenda_culturel/clear_cache.html",
|
||||
)
|
@ -42,4 +42,5 @@ django-location-field==2.7.3
|
||||
django-robots==6.1
|
||||
django-debug-toolbar==4.4.6
|
||||
django-cache-cleaner==0.1.0
|
||||
emoji==2.14.0
|
||||
emoji==2.14.0
|
||||
django-honeypot==1.2.1
|
@ -1,5 +1,5 @@
|
||||
from agenda_culturel.models import ContactMessage
|
||||
from agenda_culturel.models import Message
|
||||
|
||||
|
||||
def run():
|
||||
ContactMessage.objects.all().update(spam=True)
|
||||
Message.objects.all().update(spam=True)
|
||||
|
Loading…
x
Reference in New Issue
Block a user