Compare commits
160 Commits
filter-imp
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
e46116fe0f | ||
|
bc147016e5 | ||
|
9d84f4f630 | ||
|
f48f202e18 | ||
|
87a1168aae | ||
|
9cceba7dc0 | ||
|
2da48595eb | ||
|
f309ad6ce3 | ||
|
3d84a79c29 | ||
|
1b15ea6b2b | ||
|
280f04d22f | ||
|
20040268e7 | ||
|
15aa712199 | ||
|
0ff5dbe917 | ||
|
ba701e266a | ||
|
12896ebe93 | ||
|
caf6033496 | ||
|
db5d8496c1 | ||
|
0542e3a695 | ||
|
3cf874224f | ||
|
f9205481a8 | ||
|
6e897cf516 | ||
|
72c4e86ab1 | ||
|
c4b1ebec72 | ||
|
36b96a5557 | ||
|
875114a03b | ||
|
4de99f328c | ||
|
0da2a9995d | ||
|
1b2510c7df | ||
5a3d39d240 | |||
|
748b665a59 | ||
|
b60950b12d | ||
|
9700de054f | ||
|
d760fd4a89 | ||
|
e6f988b099 | ||
|
44eeea7505 | ||
|
1ce1976642 | ||
|
6d01b0097b | ||
|
cdbf65bf2e | ||
|
29726e0389 | ||
|
482dd9468a | ||
|
77941ed0ee | ||
|
5240f426c1 | ||
|
001a0c1552 | ||
|
0300fd3979 | ||
|
7f9ad5dd1b | ||
|
fc86738ee3 | ||
|
c9df18c822 | ||
|
49a8f4b306 | ||
|
eef4f5639c | ||
|
4b55830419 | ||
|
c3d10f01db | ||
|
d4b364c567 | ||
|
273287a250 | ||
|
045cf06e06 | ||
|
9cf1f437ef | ||
|
789a53d2e5 | ||
|
2f0c0c6f0b | ||
|
19a51bc403 | ||
e99dc06bd9 | |||
|
49c7bd5300 | ||
cc0ae8b582 | |||
3367876053 | |||
|
84ce4b0d7d | ||
|
0d62660c2b | ||
|
6b79949002 | ||
|
ad48b667be | ||
|
7c9d930e6f | ||
|
359451b9f8 | ||
|
ca6c7889a5 | ||
|
933e73de5c | ||
|
d844fb7ccf | ||
|
5343ad2cfa | ||
|
4a174d30ef | ||
|
430c7b47a2 | ||
|
63d3cb76ea | ||
|
d770cf23f0 | ||
|
cc0c798f5a | ||
|
6ceec954d8 | ||
|
2c22d62302 | ||
|
f79b1f0f89 | ||
|
3c1d51dda1 | ||
|
141949991c | ||
|
290faf0b8f | ||
|
f9f690cac7 | ||
|
5e8d9766ee | ||
|
6589c1b0c3 | ||
|
1055a36084 | ||
|
2cca0322d1 | ||
|
e5c075656c | ||
|
5ef9358b28 | ||
|
0526854d6b | ||
|
bc06b6205d | ||
|
504198b14f | ||
|
526b83ec20 | ||
|
08e66918ab | ||
|
c34fb666b2 | ||
|
a94b9a53f2 | ||
|
c1f7bfd8c4 | ||
|
b1e5414519 | ||
|
3da9a5239a | ||
|
c91cdf0c99 | ||
|
6e8f00ccbe | ||
|
a1984f60f5 | ||
|
ce95fe6504 | ||
|
dd0c037929 | ||
|
41d6b39988 | ||
|
3316d28e09 | ||
|
f7f8d9cb0c | ||
|
ced15d5113 | ||
|
70ae92854f | ||
|
02448cf4d4 | ||
|
14e25b660c | ||
|
92da6585c6 | ||
|
cd52ae0286 | ||
|
e050ce5eda | ||
|
b0b828392a | ||
|
c34abe9158 | ||
|
f52caf9855 | ||
|
bd1330cd2f | ||
|
a31bcc2764 | ||
|
91907be984 | ||
|
27ceac1e46 | ||
|
b3cba9293c | ||
|
c857294345 | ||
|
5a7cc080c7 | ||
|
37ed7c45db | ||
|
bda14c6ccb | ||
|
3d70de9c1b | ||
|
874c1779f8 | ||
|
084b3dfb25 | ||
|
ec707bf272 | ||
|
21b42e4fee | ||
|
d55d029fc7 | ||
|
1d9251946c | ||
|
e875ae626b | ||
|
63aad60260 | ||
|
27bce22670 | ||
|
1fc1fc13e1 | ||
|
252fb8c27b | ||
|
d70eca6493 | ||
|
7f1bbabebf | ||
|
c55ed5c4dc | ||
|
ac3d6796cf | ||
|
bf773686f9 | ||
|
1256adcb8a | ||
|
7120da3e28 | ||
|
4e9ac573ac | ||
|
42fb85af48 | ||
|
256fed1e2e | ||
|
d46ebeae3b | ||
|
3be7d901c8 | ||
|
5549d2172c | ||
|
674bba4a98 | ||
|
34008625d2 | ||
|
65430a2a8f | ||
|
8ef620c8e1 | ||
|
d119f1fa45 | ||
|
41f6dbc352 | ||
|
c9275c5ea0 |
@ -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 \
|
||||
|
@ -32,7 +32,7 @@ http {
|
||||
error_page 502 /static/html/500.html;
|
||||
error_page 503 /static/html/500.html;
|
||||
|
||||
if ($http_user_agent ~* (Amazonbot|meta-externalagent|ClaudeBot)) {
|
||||
if ($http_user_agent ~* "Amazonbot|meta-externalagent|ClaudeBot|ahrefsbot|semrushbot") {
|
||||
return 444;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
44
experimentations/get_la_raymonde.py
Executable file
44
experimentations/get_la_raymonde.py
Executable file
@ -0,0 +1,44 @@
|
||||
#!/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)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
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(), laraymonde.CExtractor())
|
||||
url = "https://www.raymondbar.net/"
|
||||
url_human = "https://www.raymondbar.net/"
|
||||
|
||||
try:
|
||||
events = u2e.process(url, url_human, cache = "cache-la-raymonde.html", default_values = {"location": "La Raymonde", "category": "Fêtes & Concerts"}, published = True)
|
||||
|
||||
exportfile = "events-la-raymonde.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))
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
44
experimentations/get_le_rio.py
Executable file
44
experimentations/get_le_rio.py
Executable file
@ -0,0 +1,44 @@
|
||||
#!/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)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
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))
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
@ -16,6 +16,7 @@ parent = os.path.dirname(current)
|
||||
# adding the parent directory to
|
||||
# the sys.path.
|
||||
sys.path.append(parent)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
from src.agenda_culturel.import_tasks.downloader import *
|
||||
from src.agenda_culturel.import_tasks.extractor import *
|
||||
|
44
experimentations/get_meditheques_clermont.py
Executable file
44
experimentations/get_meditheques_clermont.py
Executable file
@ -0,0 +1,44 @@
|
||||
#!/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)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
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(), iguana_agenda.CExtractor())
|
||||
url = "https://bibliotheques-clermontmetropole.eu/iguana/Service.PubContainer.cls?uuid=a4a1f992-06da-4ff4-9176-4af0a095c7d1"
|
||||
url_human = "https://bibliotheques-clermontmetropole.eu/iguana/www.main.cls?surl=AGENDA_Tout%20lagenda"
|
||||
|
||||
try:
|
||||
events = u2e.process(url, url_human, cache = "cache-mediatheques.html", default_values = {}, published = True)
|
||||
|
||||
exportfile = "events-mediatheques.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))
|
44
experimentations/get_puydedome.py
Executable file
44
experimentations/get_puydedome.py
Executable file
@ -0,0 +1,44 @@
|
||||
#!/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)
|
||||
sys.path.append(parent + "/src")
|
||||
|
||||
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(), apidae_tourisme.CExtractor())
|
||||
url = "https://widgets.apidae-tourisme.com/filter.js?widget[id]=48"
|
||||
url_human = "https://ens.puy-de-dome.fr/agenda.html"
|
||||
|
||||
try:
|
||||
events = u2e.process(url, url_human, cache = "cache-puydedome.html", default_values = {}, published = True)
|
||||
|
||||
exportfile = "events-puydedome.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)
|
||||
|
||||
|
@ -4,6 +4,7 @@ from django.db.models import Q, F
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.http import Http404
|
||||
|
||||
from django.db.models import CharField
|
||||
from django.db.models.functions import Lower
|
||||
@ -117,6 +118,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 +193,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 +238,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 +251,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 +265,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 +316,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 +331,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 +344,17 @@ 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)
|
||||
try:
|
||||
first = date.fromisocalendar(self.year, self.week, 1)
|
||||
last = date.fromisocalendar(self.year, self.week, 7)
|
||||
except:
|
||||
raise Http404()
|
||||
|
||||
super().__init__(first, last, filter)
|
||||
super().__init__(first, last, filter, qs)
|
||||
|
||||
def next_week(self):
|
||||
return self.firstdate + timedelta(days=7)
|
||||
@ -336,8 +364,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,14 @@ 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()
|
||||
elif rimport.processor == RecurrentImport.PROCESSOR.LARAYMONDE:
|
||||
extractor = laraymonde.CExtractor()
|
||||
elif rimport.processor == RecurrentImport.PROCESSOR.APIDAE:
|
||||
extractor = apidae_tourisme.CExtractor()
|
||||
elif rimport.processor == RecurrentImport.PROCESSOR.IGUANA:
|
||||
extractor = iguana_agenda.CExtractor()
|
||||
else:
|
||||
extractor = None
|
||||
|
||||
@ -173,6 +182,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 +262,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 +320,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, email=None, comments=None):
|
||||
from .db_importer import DBImporterEvents
|
||||
from agenda_culturel.models import RecurrentImport, BatchImportation
|
||||
from agenda_culturel.models import Event, Category
|
||||
@ -298,7 +329,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
|
||||
@ -323,7 +354,13 @@ def import_events_from_url(self, url, cat, tags, force=False):
|
||||
# set default values
|
||||
values = {}
|
||||
if cat is not None:
|
||||
values = {"category": cat, "tags": tags}
|
||||
values["category"] = cat
|
||||
if tags is not None:
|
||||
values["tags"] = tags
|
||||
if email is not None:
|
||||
values["email"] = email
|
||||
if comments is not None:
|
||||
values["comments"] = comments
|
||||
|
||||
# get event
|
||||
events = u2e.process(
|
||||
@ -335,7 +372,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 +389,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, email=None, comments=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, email=email, comments=comments)
|
||||
|
||||
|
||||
app.conf.beat_schedule = {
|
||||
@ -368,6 +405,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)
|
||||
@ -71,6 +73,8 @@ class DBImporterEvents:
|
||||
# conversion to Event, and return an error if it failed
|
||||
if not self.load_event(event):
|
||||
return (False, self.error_message)
|
||||
else:
|
||||
logger.warning("Event in the past, will not be imported: {}".format(event))
|
||||
|
||||
# finally save the loaded events in database
|
||||
self.save_imported()
|
||||
@ -95,7 +99,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):
|
||||
|
@ -3,6 +3,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django import forms
|
||||
from django.contrib.postgres.search import SearchQuery, SearchHeadline
|
||||
from django.db.models import Count, Q
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
from django.http import QueryDict
|
||||
from django.contrib.gis.measure import D
|
||||
@ -44,7 +46,7 @@ from .models import (
|
||||
Tag,
|
||||
Event,
|
||||
Category,
|
||||
ContactMessage,
|
||||
Message,
|
||||
DuplicatedEvents
|
||||
)
|
||||
|
||||
@ -137,7 +139,18 @@ 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
|
||||
try:
|
||||
d = float(d)
|
||||
except ValueError:
|
||||
return parent
|
||||
if d <= 0:
|
||||
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 +201,7 @@ class EventFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
def get_cleaned_data(self, name):
|
||||
|
||||
try:
|
||||
return self.form.cleaned_data[name]
|
||||
except AttributeError:
|
||||
@ -309,6 +323,13 @@ class EventFilter(django_filters.FilterSet):
|
||||
else:
|
||||
return str(self.get_cleaned_data("position")) + ' (' + str(self.get_cleaned_data("radius")) + ' km)'
|
||||
|
||||
def is_filtered_by_position_radius(self):
|
||||
return not self.get_cleaned_data("position") is None and not self.get_cleaned_data("radius") is None
|
||||
|
||||
def get_url_add_suggested_position(self, location):
|
||||
result = self.request.get_full_path()
|
||||
return result + ('&' if '?' in result else '?') + 'position=' + str(location.pk) + "&radius=" + str(location.suggested_distance)
|
||||
|
||||
|
||||
class EventFilterAdmin(django_filters.FilterSet):
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
@ -317,7 +338,7 @@ class EventFilterAdmin(django_filters.FilterSet):
|
||||
|
||||
representative = django_filters.MultipleChoiceFilter(
|
||||
label=_("Representative version"),
|
||||
choices=[(True, _("Yes")), (False, _("Non"))],
|
||||
choices=[(True, _("Yes")), (False, _("No"))],
|
||||
method="filter_by_representative",
|
||||
widget=forms.CheckboxSelectMultiple)
|
||||
|
||||
@ -349,21 +370,27 @@ class EventFilterAdmin(django_filters.FilterSet):
|
||||
fields = ["status"]
|
||||
|
||||
|
||||
class ContactMessagesFilterAdmin(django_filters.FilterSet):
|
||||
class MessagesFilterAdmin(django_filters.FilterSet):
|
||||
closed = django_filters.MultipleChoiceFilter(
|
||||
label="Status",
|
||||
label=_("Status"),
|
||||
choices=((True, _("Closed")), (False, _("Open"))),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
spam = django_filters.MultipleChoiceFilter(
|
||||
label="Spam",
|
||||
label=_("Spam"),
|
||||
choices=((True, _("Spam")), (False, _("Non spam"))),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
|
||||
message_type = django_filters.MultipleChoiceFilter(
|
||||
label=_("Type"),
|
||||
choices=Message.TYPE.choices,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ContactMessage
|
||||
fields = ["closed", "spam"]
|
||||
model = Message
|
||||
fields = ["closed", "spam", "message_type"]
|
||||
|
||||
|
||||
class SimpleSearchEventFilter(django_filters.FilterSet):
|
||||
@ -379,6 +406,24 @@ class SimpleSearchEventFilter(django_filters.FilterSet):
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
|
||||
past = django_filters.ChoiceFilter(
|
||||
label=_("In the past"),
|
||||
choices=[(False, _("No")), (True, _("Yes"))],
|
||||
null_label=None,
|
||||
empty_label=None,
|
||||
method="in_past",
|
||||
widget=forms.Select)
|
||||
|
||||
def in_past(self, queryset, name, value):
|
||||
if value and value == "True":
|
||||
now = date.today()
|
||||
qs = queryset.filter(start_day__lt=now).order_by("-start_day", "-start_time")
|
||||
else:
|
||||
start = date.today() + timedelta(days=-2)
|
||||
qs = queryset.filter(start_day__gte=start).order_by("start_day", "start_time")
|
||||
return qs
|
||||
|
||||
|
||||
def custom_filter(self, queryset, name, value):
|
||||
search_query = SearchQuery(value, config="french")
|
||||
qs = queryset.filter(
|
||||
|
@ -13,16 +13,21 @@ from django.forms import (
|
||||
BooleanField,
|
||||
HiddenInput,
|
||||
ModelChoiceField,
|
||||
EmailField
|
||||
)
|
||||
from django.forms import formset_factory
|
||||
|
||||
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
|
||||
|
||||
from .utils import PlaceGuesser
|
||||
from .models import (
|
||||
Event,
|
||||
RecurrentImport,
|
||||
CategorisationRule,
|
||||
Place,
|
||||
Category,
|
||||
Tag
|
||||
Tag,
|
||||
Message
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
@ -31,7 +36,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
|
||||
@ -73,7 +77,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()
|
||||
@ -95,9 +99,12 @@ class TagForm(ModelForm):
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ["name", "description", "in_included_suggestions", "in_excluded_suggestions", "principal"]
|
||||
widgets = {
|
||||
"name": HiddenInput()
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if "name" in kwargs["initial"]:
|
||||
self.fields["name"].widget = HiddenInput()
|
||||
|
||||
|
||||
class TagRenameForm(Form):
|
||||
required_css_class = 'required'
|
||||
@ -124,7 +131,42 @@ class TagRenameForm(Form):
|
||||
def is_force(self):
|
||||
return "force" in self.fields and self.cleaned_data["force"] == True
|
||||
|
||||
class URLSubmissionForm(Form):
|
||||
|
||||
class SimpleContactForm(GroupFormMixin, Form):
|
||||
email = EmailField(
|
||||
label=_("Your email"),
|
||||
help_text=_("Your email address"),
|
||||
max_length=254,
|
||||
required=False
|
||||
)
|
||||
|
||||
comments = CharField(
|
||||
label=_("Comments"),
|
||||
help_text=_("Your message for the moderation team (comments, clarifications, requests...)"),
|
||||
widget=Textarea,
|
||||
max_length=2048,
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
is_authenticated = "is_authenticated" in kwargs and kwargs["is_authenticated"]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not is_authenticated:
|
||||
self.add_group('communication',
|
||||
_('Receive notification of publication or leave a message for moderation'),
|
||||
maskable=True,
|
||||
default_masked=True)
|
||||
self.fields["email"].group_id = 'communication'
|
||||
self.fields["comments"].group_id = 'communication'
|
||||
else:
|
||||
del self.fields["email"]
|
||||
del self.fields["comments"]
|
||||
|
||||
|
||||
|
||||
|
||||
class URLSubmissionForm(GroupFormMixin, Form):
|
||||
required_css_class = 'required'
|
||||
|
||||
url = URLField(max_length=512)
|
||||
@ -142,11 +184,20 @@ class URLSubmissionForm(Form):
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
is_authenticated = kwargs.pop("is_authenticated", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["tags"].choices = Tag.get_tag_groups(all=True)
|
||||
|
||||
self.add_group('event', _('Event'))
|
||||
self.fields["url"].group_id = 'event'
|
||||
self.fields["category"].group_id = 'event'
|
||||
self.fields["tags"].group_id = 'event'
|
||||
|
||||
|
||||
class URLSubmissionFormWithContact(SimpleContactForm, URLSubmissionForm):
|
||||
pass
|
||||
|
||||
URLSubmissionFormSet = formset_factory(URLSubmissionForm, extra=9, min_num=1)
|
||||
|
||||
class DynamicArrayWidgetURLs(DynamicArrayWidget):
|
||||
template_name = "agenda_culturel/widgets/widget-urls.html"
|
||||
@ -159,12 +210,20 @@ class DynamicArrayWidgetTags(DynamicArrayWidget):
|
||||
class RecurrentImportForm(ModelForm):
|
||||
required_css_class = 'required'
|
||||
|
||||
defaultTags = MultipleChoiceField(
|
||||
label=_("Tags"),
|
||||
initial=None,
|
||||
choices=[],
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RecurrentImport
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"defaultTags": DynamicArrayWidgetTags(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["defaultTags"].choices = Tag.get_tag_groups(all=True)
|
||||
|
||||
|
||||
class CategorisationRuleImportForm(ModelForm):
|
||||
@ -180,6 +239,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"),
|
||||
@ -196,7 +256,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(
|
||||
@ -245,7 +309,14 @@ class EventForm(GroupFormMixin, ModelForm):
|
||||
self.fields['end_day'].group_id = 'end'
|
||||
self.fields['end_time'].group_id = 'end'
|
||||
|
||||
self.add_group('recurrences', _('This is a recurring event'), maskable=True, default_masked=True)
|
||||
self.add_group('recurrences',
|
||||
_('This is a recurring event'),
|
||||
maskable=True,
|
||||
default_masked=not (self.instance and
|
||||
self.instance.recurrences and
|
||||
self.instance.recurrences.rrules and
|
||||
len(self.instance.recurrences.rrules) > 0))
|
||||
|
||||
self.fields['recurrences'].group_id = 'recurrences'
|
||||
|
||||
self.add_group('details', _('Details'))
|
||||
@ -261,6 +332,8 @@ class EventForm(GroupFormMixin, ModelForm):
|
||||
self.fields['local_image'].group_id = 'illustration'
|
||||
self.fields['image_alt'].group_id = 'illustration'
|
||||
|
||||
self.add_group('urls', _('URLs'))
|
||||
self.fields["reference_urls"].group_id = 'urls'
|
||||
|
||||
if is_authenticated:
|
||||
self.add_group('meta-admin', _('Meta information'))
|
||||
@ -308,7 +381,7 @@ class EventForm(GroupFormMixin, ModelForm):
|
||||
super().clean()
|
||||
|
||||
# when cloning an existing event, we need to copy the local image
|
||||
if self.cleaned_data['local_image'] is None and \
|
||||
if ((not 'local_image' in self.cleaned_data) or (self.cleaned_data['local_image'] is None)) and \
|
||||
not self.cleaned_data['old_local_image'] is None and \
|
||||
self.cleaned_data['old_local_image'] != "":
|
||||
basename = self.cleaned_data['old_local_image']
|
||||
@ -317,6 +390,9 @@ class EventForm(GroupFormMixin, ModelForm):
|
||||
self.cleaned_data['local_image'] = File(name=basename, file=open(old, "rb"))
|
||||
|
||||
|
||||
class EventFormWithContact(SimpleContactForm, EventForm):
|
||||
pass
|
||||
|
||||
class MultipleChoiceFieldAcceptAll(MultipleChoiceField):
|
||||
def validate(self, value):
|
||||
pass
|
||||
@ -347,6 +423,9 @@ class EventModerateForm(ModelForm):
|
||||
"exact_location",
|
||||
"tags"
|
||||
]
|
||||
widgets = {
|
||||
"status": RadioSelect
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -470,14 +549,14 @@ class FixDuplicates(Form):
|
||||
class SelectEventInList(Form):
|
||||
required_css_class = 'required'
|
||||
|
||||
event = ChoiceField()
|
||||
event = ChoiceField(label=_('Event'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
events = kwargs.pop("events", None)
|
||||
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
|
||||
]
|
||||
|
||||
|
||||
@ -525,17 +604,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>"
|
||||
@ -586,7 +665,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"'
|
||||
@ -618,7 +697,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
|
||||
@ -702,7 +781,7 @@ class EventAddPlaceForm(Form):
|
||||
return self.instance
|
||||
|
||||
|
||||
class PlaceForm(ModelForm):
|
||||
class PlaceForm(GroupFormMixin, ModelForm):
|
||||
required_css_class = 'required'
|
||||
|
||||
apply_to_all = BooleanField(
|
||||
@ -718,13 +797,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 MessageForm(ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = ["subject", "name", "email", "message", "related_event"]
|
||||
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")
|
@ -0,0 +1,103 @@
|
||||
from ..generic_extractors import *
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
|
||||
# A class dedicated to get events from apidae-tourisme widgets
|
||||
class CExtractor(TwoStepsExtractorNoPause):
|
||||
|
||||
|
||||
def build_event_url_list(self, content, infuture_days=180):
|
||||
|
||||
# Get line starting with wrapper.querySelector(".results_agenda").innerHTML = "
|
||||
# split using "=" and keep the end
|
||||
# strip it, and remove the first character (") and the two last ones (";)
|
||||
# remove the escapes and parse the contained html
|
||||
for line in content.split("\n"):
|
||||
if line.startswith('wrapper.querySelector(".results_agenda").innerHTML = "'):
|
||||
html = ('"'.join(line.split('"')[3:])).replace('\\"', '"').replace('\\n', "\n").replace('\\/', '/')
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
links = soup.select('a.widgit_result')
|
||||
for l in links:
|
||||
self.add_event_url(l["data-w-href"])
|
||||
break
|
||||
|
||||
|
||||
def add_event_from_content(
|
||||
self,
|
||||
event_content,
|
||||
event_url,
|
||||
url_human=None,
|
||||
default_values=None,
|
||||
published=False,
|
||||
):
|
||||
# check for htag
|
||||
for line in event_content.split("\n"):
|
||||
if line.strip().startswith("window.location.hash"):
|
||||
ref = line.split('"')[1]
|
||||
break
|
||||
|
||||
# check for content
|
||||
for line in event_content.split("\n"):
|
||||
if line.startswith('detailsWrapper.innerHTML ='):
|
||||
html = ('"'.join(line.split('"')[1:])).replace('\\"', '"').replace('\\n', "\n").replace('\\/', '/')
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
title = soup.select_one('h2.widgit_title').text.strip()
|
||||
image = soup.select_one('img')
|
||||
image_alt = image["alt"]
|
||||
image = image["src"]
|
||||
description = soup.select('div.desc')
|
||||
description = '\n'.join([d.text for d in description])
|
||||
openings = soup.select_one('.openings .mts').text.strip().split("\n")[0]
|
||||
start_time = None
|
||||
end_time = None
|
||||
if "tous les" in openings:
|
||||
start_day = None
|
||||
else:
|
||||
start_day = Extractor.parse_french_date(openings)
|
||||
details = openings.split("de")
|
||||
if len(details) > 1:
|
||||
hours = details[1].split("à")
|
||||
start_time = Extractor.parse_french_time(hours[0])
|
||||
if len(hours) > 1:
|
||||
end_time = Extractor.parse_french_time(hours[1])
|
||||
|
||||
contact = soup.select_one(".contact")
|
||||
sa = False
|
||||
location = []
|
||||
for c in contact.children:
|
||||
if c.name == 'h2' and c.text.strip() == "Adresse":
|
||||
sa = True
|
||||
else:
|
||||
if c.name == 'h2' and sa:
|
||||
break
|
||||
if c.name == 'p' and sa:
|
||||
e = c.text.strip()
|
||||
if e != "":
|
||||
location.append(e)
|
||||
|
||||
location = ', '.join(location)
|
||||
|
||||
websites = soup.select("a.website")
|
||||
event_url = url_human + "#" + ref
|
||||
|
||||
self.add_event_with_props(
|
||||
default_values,
|
||||
event_url,
|
||||
title,
|
||||
None,
|
||||
start_day,
|
||||
location,
|
||||
description,
|
||||
[],
|
||||
recurrences=None,
|
||||
uuids=[event_url],
|
||||
url_human=event_url,
|
||||
start_time=start_time,
|
||||
end_day=start_day,
|
||||
end_time=end_time,
|
||||
published=published,
|
||||
image=image,
|
||||
image_alt=image_alt
|
||||
)
|
||||
return
|
@ -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")
|
||||
|
||||
|
||||
|
@ -0,0 +1,114 @@
|
||||
from ..generic_extractors import *
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# A class dedicated to get events from Raymond Bar
|
||||
# URL: https://www.raymondbar.net/
|
||||
class CExtractor(TwoStepsExtractorNoPause):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def guess_category(self, category):
|
||||
if "Cinéma" in category:
|
||||
return "Cinéma"
|
||||
if "Conférence" in category or "Rencontres" in category:
|
||||
return "Rencontres & débats"
|
||||
if "Lecture" in category or "Conte" in category:
|
||||
return "Spectacles"
|
||||
if "Atelier" in category or "Jeux" in category or "":
|
||||
return "Animations & Ateliers"
|
||||
if "Numérique" in category:
|
||||
return "Rendez-vous locaux"
|
||||
|
||||
return "Sans catégorie"
|
||||
|
||||
|
||||
def guess_tags_from_category(self, category):
|
||||
tags = []
|
||||
if "Lecture" in category:
|
||||
tags.append("📖 lecture")
|
||||
if "Jeux" in category:
|
||||
tags.append("🎲 jeux")
|
||||
|
||||
return tags
|
||||
|
||||
def build_event_url_list(self, content, infuture_days=180):
|
||||
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
|
||||
root_address_human = self.url_human.split('?')[0]
|
||||
root_address = self.url.split('Service')[0]
|
||||
|
||||
items = soup.select("li.listItem")
|
||||
if items:
|
||||
for item in items:
|
||||
elems = item["onclick"].split('"')
|
||||
v = elems[3].split('^')[1]
|
||||
contentItem = elems[1]
|
||||
multidate = item.select_one('.until.maindate').text != ''
|
||||
if not multidate:
|
||||
url_human = root_address_human + '?p=*&v=' + v + "#contentitem=" + contentItem
|
||||
url = root_address + 'Service.PubItem.cls?action=get&instance=*&uuid=' + contentItem
|
||||
self.add_event_url(url)
|
||||
self.add_event_url_human(url, url_human)
|
||||
|
||||
|
||||
def add_event_from_content(
|
||||
self,
|
||||
event_content,
|
||||
event_url,
|
||||
url_human=None,
|
||||
default_values=None,
|
||||
published=False,
|
||||
):
|
||||
|
||||
root_address_human = "https://" + urlparse(self.url_human).netloc + "/"
|
||||
|
||||
soup = BeautifulSoup(event_content, "xml")
|
||||
|
||||
|
||||
title = soup.select_one("Title").text
|
||||
content = soup.select_one("Content").text
|
||||
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
|
||||
image = root_address_human + soup.select_one(".image img")["src"]
|
||||
description = soup.select_one(".rightcolumn .content").text
|
||||
location = soup.select_one(".infos .location").text
|
||||
public = soup.select_one(".infos .public").text
|
||||
start_day = Extractor.parse_french_date(soup.select_one(".infos .date .from").text)
|
||||
start_time = Extractor.parse_french_time(soup.select_one(".infos .date .time").text)
|
||||
acces = soup.select_one(".infos .acces").text
|
||||
category = soup.select_one(".rightcolumn .category").text
|
||||
infos = soup.select_one('.infos').text
|
||||
|
||||
description = description + "\n" + infos
|
||||
|
||||
tags = self.guess_tags_from_category(category)
|
||||
category = self.guess_category(category)
|
||||
if "Tout-petits" in public or "Jeunesse" in public:
|
||||
tags.append("🎈 jeune public")
|
||||
if "Accès libre" in acces:
|
||||
tags.append("💶 gratuit")
|
||||
|
||||
self.add_event_with_props(
|
||||
default_values,
|
||||
event_url,
|
||||
title,
|
||||
category,
|
||||
start_day,
|
||||
location,
|
||||
description,
|
||||
tags,
|
||||
recurrences=None,
|
||||
uuids=[event_url],
|
||||
url_human=event_url,
|
||||
start_time=start_time,
|
||||
end_day=None,
|
||||
end_time=None,
|
||||
published=published,
|
||||
image=image,
|
||||
image_alt=""
|
||||
)
|
@ -106,6 +106,16 @@ class CExtractor(TwoStepsExtractor):
|
||||
description = soup.select("#descspec")
|
||||
if description and len(description) > 0:
|
||||
description = description[0].get_text().replace("Lire plus...", "")
|
||||
# on ajoute éventuellement les informations complémentaires
|
||||
|
||||
d_suite = ""
|
||||
for d in ["#typespec", "#dureespec", "#lieuspec", ".lkuncontdroitespec"]:
|
||||
comp_desc = soup.select(d)
|
||||
if comp_desc and len(comp_desc) > 0:
|
||||
for desc in comp_desc:
|
||||
d_suite += "\n\n" + desc.get_text()
|
||||
if d_suite != "":
|
||||
description += "\n\n> Informations complémentaires:" + d_suite
|
||||
else:
|
||||
description = None
|
||||
|
||||
|
@ -0,0 +1,67 @@
|
||||
from ..generic_extractors import *
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
|
||||
# A class dedicated to get events from Raymond Bar
|
||||
# URL: https://www.raymondbar.net/
|
||||
class CExtractor(TwoStepsExtractorNoPause):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def build_event_url_list(self, content, infuture_days=180):
|
||||
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
|
||||
links = soup.select(".showsList .showMore")
|
||||
if links:
|
||||
for l in links:
|
||||
print(l["href"])
|
||||
self.add_event_url(l["href"])
|
||||
|
||||
|
||||
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(".showDesc h4 a.summary").text
|
||||
start_day = soup.select_one(".showDate .value-title")
|
||||
|
||||
if not start_day is None:
|
||||
start_day = start_day["title"]
|
||||
if not start_day is None:
|
||||
start_day = start_day.split("T")[0]
|
||||
if start_day is None:
|
||||
print("impossible de récupérer la date")
|
||||
return
|
||||
description = soup.select_one('.showDetails.description').text
|
||||
image = soup.select('.showDetails.description img')
|
||||
if not image is None:
|
||||
image_alt = image[-1]["alt"]
|
||||
image = image[-1]["src"]
|
||||
|
||||
self.add_event_with_props(
|
||||
default_values,
|
||||
event_url,
|
||||
title,
|
||||
None,
|
||||
start_day,
|
||||
None,
|
||||
description,
|
||||
[],
|
||||
recurrences=None,
|
||||
uuids=[event_url],
|
||||
url_human=event_url,
|
||||
start_time=None,
|
||||
end_day=None,
|
||||
end_time=None,
|
||||
published=published,
|
||||
image=image,
|
||||
image_alt=image_alt
|
||||
)
|
90
src/agenda_culturel/import_tasks/custom_extractors/lerio.py
Normal file
90
src/agenda_culturel/import_tasks/custom_extractors/lerio.py
Normal file
@ -0,0 +1,90 @@
|
||||
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:
|
||||
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
|
||||
)
|
@ -88,7 +88,7 @@ class CExtractor(TwoStepsExtractor):
|
||||
image = image[0]["src"]
|
||||
else:
|
||||
image = None
|
||||
description = soup.select(".mec-event-content")[0].get_text(separator=" ")
|
||||
description = soup.select(".mec-event-content .mec-single-event-description")[0].get_text(separator=" ")
|
||||
|
||||
url_human = event_url
|
||||
|
||||
|
@ -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")
|
||||
|
@ -2,8 +2,7 @@ from abc import ABC, abstractmethod
|
||||
from datetime import datetime, time, date, timedelta
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Extractor(ABC):
|
||||
@ -49,7 +48,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 +72,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
|
||||
@ -170,12 +176,17 @@ class Extractor(ABC):
|
||||
image=None,
|
||||
image_alt=None,
|
||||
):
|
||||
comments = ''
|
||||
if title is None:
|
||||
print("ERROR: cannot import an event without name")
|
||||
return
|
||||
print("WARNING: cannot publish an event without name")
|
||||
published = False
|
||||
title = _('Unknown title')
|
||||
if start_day is None:
|
||||
print("ERROR: cannot import an event without start day")
|
||||
return
|
||||
print("WARNING: cannot publish an event without start day")
|
||||
published = False
|
||||
start_day = datetime.now().date()
|
||||
comments = 'Warning: the date has not been imported correctly.'
|
||||
title += ' - Warning: the date has not been imported correctly.'
|
||||
|
||||
tags_default = self.default_value_if_exists(default_values, "tags")
|
||||
if not tags_default:
|
||||
@ -193,7 +204,14 @@ class Extractor(ABC):
|
||||
"published": published,
|
||||
"image": image,
|
||||
"image_alt": image_alt,
|
||||
"email": self.default_value_if_exists(default_values, "email"),
|
||||
"comments": self.default_value_if_exists(default_values, "comments"),
|
||||
}
|
||||
if event["comments"] is None:
|
||||
event["comments"] = comments
|
||||
else:
|
||||
event["comments"] += '\n' + comments
|
||||
|
||||
# TODO: pourquoi url_human et non reference_url
|
||||
if url_human is not None:
|
||||
event["url_human"] = url_human
|
||||
@ -240,6 +258,28 @@ class Extractor(ABC):
|
||||
from .extractor_ggcal_link import GoogleCalendarLinkEventExtractor
|
||||
|
||||
if single_event:
|
||||
return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor()]
|
||||
return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]
|
||||
else:
|
||||
return [ICALExtractor(), FacebookEventExtractor(), GoogleCalendarLinkEventExtractor()]
|
||||
return [ICALExtractor(), FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]
|
||||
|
||||
|
||||
# A class that only produce a not found event
|
||||
class EventNotFoundExtractor(Extractor):
|
||||
|
||||
def extract(
|
||||
self, content, url, url_human=None, default_values=None, published=False
|
||||
):
|
||||
self.set_header(url)
|
||||
self.clear_events()
|
||||
|
||||
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 " + url,
|
||||
[], [url], published=False, url_human=url)
|
||||
|
||||
return self.get_structure()
|
||||
|
||||
|
||||
def clean_url(url):
|
||||
return url
|
||||
|
||||
|
@ -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]
|
||||
|
||||
|
@ -102,20 +102,22 @@ class TwoStepsExtractor(Extractor):
|
||||
self.event_urls.append(url)
|
||||
return True
|
||||
|
||||
def add_event_start_day(self, url, start_day):
|
||||
def add_event_property(self, url, key, value):
|
||||
if url not in self.event_properties:
|
||||
self.event_properties[url] = {}
|
||||
self.event_properties[url]["start_day"] = start_day
|
||||
self.event_properties[url][key] = value
|
||||
|
||||
def add_event_url_human(self, url, url_human):
|
||||
self.add_event_property(url, "url_human", url_human)
|
||||
|
||||
def add_event_start_day(self, url, start_day):
|
||||
self.add_event_property(url, "start_day", start_day)
|
||||
|
||||
def add_event_start_time(self, url, start_time):
|
||||
if url not in self.event_properties:
|
||||
self.event_properties[url] = {}
|
||||
self.event_properties[url]["start_time"] = start_time
|
||||
self.add_event_property(url, "start_time", start_time)
|
||||
|
||||
def add_event_title(self, url, title):
|
||||
if url not in self.event_properties:
|
||||
self.event_properties[url] = {}
|
||||
self.event_properties[url]["title"] = title
|
||||
self.add_event_property(url, "title", title)
|
||||
|
||||
def add_event_tag(self, url, tag):
|
||||
if url not in self.event_properties:
|
||||
@ -125,14 +127,10 @@ class TwoStepsExtractor(Extractor):
|
||||
self.event_properties[url]["tags"].append(tag)
|
||||
|
||||
def add_event_category(self, url, cat):
|
||||
if url not in self.event_properties:
|
||||
self.event_properties[url] = {}
|
||||
self.event_properties[url]["category"] = cat
|
||||
self.add_event_property(url, "category", cat)
|
||||
|
||||
def add_event_location(self, url, loc):
|
||||
if url not in self.event_properties:
|
||||
self.event_properties[url] = {}
|
||||
self.event_properties[url]["location"] = loc
|
||||
self.add_event_property(url, "location", loc)
|
||||
|
||||
def add_event_with_props(
|
||||
self,
|
||||
@ -168,6 +166,8 @@ class TwoStepsExtractor(Extractor):
|
||||
category = self.event_properties[event_url]["category"]
|
||||
if "location" in self.event_properties[event_url]:
|
||||
location = self.event_properties[event_url]["location"]
|
||||
if "url_human" in self.event_properties[event_url]:
|
||||
url_human = self.event_properties[event_url]["url_human"]
|
||||
|
||||
self.add_event(
|
||||
default_values,
|
||||
@ -221,6 +221,7 @@ class TwoStepsExtractor(Extractor):
|
||||
self.clear_events()
|
||||
|
||||
self.url = url
|
||||
self.url_human = url_human
|
||||
self.event_urls = []
|
||||
self.event_properties.clear()
|
||||
|
||||
@ -264,9 +265,13 @@ 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
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
from .downloader import *
|
||||
from .extractor import *
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class URL2Events:
|
||||
def __init__(
|
||||
@ -29,8 +34,9 @@ class URL2Events:
|
||||
else:
|
||||
# if the extractor is not defined, use a list of default extractors
|
||||
for e in Extractor.get_default_extractors(self.single_event):
|
||||
logger.warning('Extractor::' + type(e).__name__)
|
||||
e.set_downloader(self.downloader)
|
||||
events = e.extract(content, url, url_human, default_values, published)
|
||||
if events is not None:
|
||||
if events is not None and len(events) > 0:
|
||||
return events
|
||||
return None
|
||||
|
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-27 18:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda_culturel', '0119_alter_tag_options_alter_event_category_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='referencelocation',
|
||||
name='suggested_distance',
|
||||
field=models.IntegerField(default=None, help_text='If this distance is given, this location is part of the suggested filters.', null=True, verbose_name='Suggested distance (km)'),
|
||||
),
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.9 on 2024-11-27 22:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda_culturel', '0120_referencelocation_suggested_distance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contactmessage',
|
||||
name='related_event',
|
||||
field=models.ForeignKey(default=None, help_text='The message is associated with this event.', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='agenda_culturel.event', verbose_name='Related event'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
18
src/agenda_culturel/migrations/0131_message_message_type.py
Normal file
18
src/agenda_culturel/migrations/0131_message_message_type.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.9 on 2024-12-29 11:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda_culturel', '0130_recurrentimport_forcelocation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='message_type',
|
||||
field=models.CharField(choices=[('from_contributor', 'From contributor'), ('import_process', 'Import process'), ('contact_form', 'Contact form'), ('event_report', 'Event report')], default=None, max_length=20, null=True, verbose_name='Type'),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.9 on 2024-12-29 16:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda_culturel', '0131_message_message_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='message_type',
|
||||
field=models.CharField(choices=[('from_contributor', 'From contributor'), ('import_process', 'Import process'), ('contact_form', 'Contact form'), ('event_report', 'Event report'), ('from_contrib_no_msg', 'From contributor (without message)')], default=None, max_length=20, null=True, verbose_name='Type'),
|
||||
),
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.9 on 2025-01-09 21:21
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_better_admin_arrayfield.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda_culturel', '0132_alter_message_message_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='reference_urls',
|
||||
field=django_better_admin_arrayfield.models.fields.ArrayField(base_field=models.URLField(max_length=512), blank=True, null=True, size=None, verbose_name='Online sources or ticketing'),
|
||||
),
|
||||
]
|
18
src/agenda_culturel/migrations/0134_alter_tag_principal.py
Normal file
18
src/agenda_culturel/migrations/0134_alter_tag_principal.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.9 on 2025-01-11 13:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda_culturel', '0133_alter_event_reference_urls'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='principal',
|
||||
field=models.BooleanField(default=False, help_text='This tag is highlighted as a main tag for visitors, particularly in the filter.', verbose_name='Principal'),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.9 on 2025-01-18 15:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda_culturel', '0134_alter_tag_principal'),
|
||||
]
|
||||
|
||||
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'), ('raymonde', 'La Raymonde'), ('apidae', 'Agenda apidae tourisme')], default='ical', max_length=20, verbose_name='Processor'),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.9 on 2025-01-19 13:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda_culturel', '0135_alter_recurrentimport_processor'),
|
||||
]
|
||||
|
||||
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'), ('raymonde', 'La Raymonde'), ('apidae', 'Agenda apidae tourisme'), ('iguana', 'Agenda iguana (médiathèques)')], default='ical', max_length=20, verbose_name='Processor'),
|
||||
),
|
||||
]
|
@ -10,7 +10,16 @@ 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
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
|
||||
import uuid
|
||||
|
||||
import hashlib
|
||||
import urllib.request
|
||||
@ -20,6 +29,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
|
||||
@ -196,7 +206,7 @@ class Tag(models.Model):
|
||||
principal = models.BooleanField(
|
||||
verbose_name=_("Principal"),
|
||||
help_text=_("This tag is highlighted as a main tag for visitors, particularly in the filter."),
|
||||
default=True,
|
||||
default=False,
|
||||
)
|
||||
|
||||
in_excluded_suggestions = models.BooleanField(
|
||||
@ -222,6 +232,15 @@ class Tag(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse("view_tag", kwargs={"t": self.name})
|
||||
|
||||
|
||||
def clear_cache():
|
||||
for exclude in [False, True]:
|
||||
for include in [False, True]:
|
||||
for nb_suggestions in [10]:
|
||||
id_cache = 'all_tags ' + str(exclude) + ' ' + str(include) + ' ' + str(nb_suggestions)
|
||||
id_cache = hashlib.md5(id_cache.encode("utf8")).hexdigest()
|
||||
cache.delete(id_cache)
|
||||
|
||||
def get_tag_groups(nb_suggestions=10, exclude=False, include=False, all=False):
|
||||
id_cache = 'all_tags ' + str(exclude) + ' ' + str(include) + ' ' + str(nb_suggestions)
|
||||
id_cache = hashlib.md5(id_cache.encode("utf8")).hexdigest()
|
||||
@ -230,6 +249,7 @@ class Tag(models.Model):
|
||||
if not result:
|
||||
|
||||
free_tags = Event.get_all_tags(False)
|
||||
f_tags = [t["tag"] for t in free_tags]
|
||||
|
||||
obj_tags = Tag.objects
|
||||
|
||||
@ -250,6 +270,8 @@ class Tag(models.Model):
|
||||
nb_suggestions = len(obj_tags)
|
||||
|
||||
tags = [{"tag": t["tag"], "count": 1000000 if t["tag"] in obj_tags else t["count"]} for t in free_tags]
|
||||
tags += [{"tag": o, "count": 0} for o in Tag.objects.filter(~Q(name__in=f_tags)).values_list("name", flat=True)]
|
||||
|
||||
|
||||
tags.sort(key=lambda x: -x["count"])
|
||||
|
||||
@ -284,6 +306,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
|
||||
@ -398,6 +424,7 @@ class DuplicatedEvents(models.Model):
|
||||
|
||||
|
||||
class ReferenceLocation(models.Model):
|
||||
|
||||
name = models.CharField(verbose_name=_("Name"), help_text=_("Name of the location"), unique=True, null=False)
|
||||
location = LocationField(based_fields=["name"], zoom=12, default=Point(3.08333, 45.783329), srid=4326)
|
||||
main = models.IntegerField(
|
||||
@ -405,6 +432,12 @@ class ReferenceLocation(models.Model):
|
||||
help_text=_("This location is one of the main locations (shown first higher values)."),
|
||||
default=0,
|
||||
)
|
||||
suggested_distance = models.IntegerField(
|
||||
verbose_name=_("Suggested distance (km)"),
|
||||
help_text=_("If this distance is given, this location is part of the suggested filters."),
|
||||
null=True,
|
||||
default=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Reference location")
|
||||
@ -427,8 +460,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"),
|
||||
@ -451,6 +485,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:
|
||||
@ -536,7 +575,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})
|
||||
|
||||
|
||||
|
||||
@ -551,6 +590,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)
|
||||
|
||||
@ -664,8 +736,7 @@ class Event(models.Model):
|
||||
)
|
||||
reference_urls = ArrayField(
|
||||
models.URLField(max_length=512),
|
||||
verbose_name=_("URLs"),
|
||||
help_text=_("List of all the urls where this event can be found."),
|
||||
verbose_name=_("Online sources or ticketing"),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
@ -685,6 +756,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)
|
||||
@ -702,15 +777,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)
|
||||
|
||||
@ -720,6 +786,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)
|
||||
|
||||
@ -757,9 +832,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 []
|
||||
@ -840,6 +942,32 @@ class Event(models.Model):
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_nb_not_moderated(first_day, nb_mod_days=21, nb_classes=4):
|
||||
window_end = first_day + timedelta(days=nb_mod_days)
|
||||
nb_not_moderated = Event.objects.filter(~Q(status=Event.STATUS.TRASH)). \
|
||||
filter(Q(start_day__gte=first_day)&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 = [first_day + timedelta(days=x) for x in range(0, nb_mod_days)]
|
||||
nb_not_moderated = [{"start_day": d,
|
||||
"is_today": d == first_day,
|
||||
"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]
|
||||
return [nb_not_moderated[x:x + 7] for x in range(0, len(nb_not_moderated), 7)]
|
||||
|
||||
|
||||
def nb_draft_events():
|
||||
return Event.objects.filter(status=Event.STATUS.DRAFT).count()
|
||||
|
||||
@ -851,19 +979,27 @@ 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):
|
||||
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
|
||||
@ -889,6 +1025,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
|
||||
|
||||
@ -899,12 +1041,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(
|
||||
@ -916,10 +1062,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:
|
||||
@ -1017,7 +1166,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
|
||||
@ -1036,6 +1185,42 @@ class Event(models.Model):
|
||||
if not self.category or self.category.name == Category.default_name:
|
||||
CategorisationRule.apply_rules(self)
|
||||
|
||||
|
||||
def get_contributor_message(self):
|
||||
types = [Message.TYPE.FROM_CONTRIBUTOR, Message.TYPE.FROM_CONTRIBUTOR_NO_MSG]
|
||||
if self.other_versions is None or self.other_versions.representative is None:
|
||||
return Message.objects.filter(related_event=self.pk, message_type__in=types, closed=False)
|
||||
else:
|
||||
return Message.objects.filter(related_event__in=self.other_versions.get_duplicated(), message_type__in=types, closed=False)
|
||||
|
||||
|
||||
def notify_if_required(self, request):
|
||||
notif = False
|
||||
if self.status != Event.STATUS.DRAFT:
|
||||
messages = self.get_contributor_message()
|
||||
logger.warning("messages: ")
|
||||
logger.warning(messages)
|
||||
if messages:
|
||||
for message in messages:
|
||||
if message and not message.closed and message.email and message.email != "":
|
||||
# send email
|
||||
context = {"sitename": Site.objects.get_current(request).name, 'event_title': self.title }
|
||||
if self.status == Event.STATUS.PUBLISHED:
|
||||
context["url"] = request.build_absolute_uri(self.get_absolute_url())
|
||||
subject = _('Your event has been published')
|
||||
body = render_to_string("agenda_culturel/emails/published.txt", context)
|
||||
else:
|
||||
subject = _('Your message has not been retained')
|
||||
body = render_to_string("agenda_culturel/emails/retained.txt", context)
|
||||
|
||||
send_mail(subject, body, None, [message.email])
|
||||
message.closed = True
|
||||
message.save()
|
||||
notif = True
|
||||
|
||||
return notif
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.prepare_save()
|
||||
|
||||
@ -1069,6 +1254,21 @@ class Event(models.Model):
|
||||
# first save the current object
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# notify only if required (and request is known)
|
||||
if "request" in kwargs:
|
||||
self.notify_if_required(kwargs.get("request"))
|
||||
|
||||
# clear cache
|
||||
for is_auth in [False, True]:
|
||||
key = make_template_fragment_key("event_body", [is_auth, self])
|
||||
cache.delete(key)
|
||||
|
||||
# save message if required
|
||||
if self.has_message():
|
||||
msg = self.get_message()
|
||||
msg.related_event = self
|
||||
msg.save()
|
||||
|
||||
# then if its a clone, update the representative
|
||||
if clone:
|
||||
self.other_versions.representative = self
|
||||
@ -1085,6 +1285,8 @@ class Event(models.Model):
|
||||
def from_structure(event_structure, import_source=None):
|
||||
# organisers is a manytomany relation thus cannot be initialised before creation of the event
|
||||
organisers = event_structure.pop('organisers', None)
|
||||
email = event_structure.pop('email', None)
|
||||
comments = event_structure.pop('comments', None)
|
||||
|
||||
if "category" in event_structure and event_structure["category"] is not None:
|
||||
try:
|
||||
@ -1114,8 +1316,6 @@ class Event(models.Model):
|
||||
and event_structure["last_modified"] is not None
|
||||
):
|
||||
d = datetime.fromisoformat(event_structure["last_modified"])
|
||||
if d.year == 2024 and d.month > 2:
|
||||
logger.warning("last modified {}".format(d))
|
||||
if d.tzinfo is None or d.tzinfo.utcoffset(d) is None:
|
||||
d = timezone.make_aware(d, timezone.get_default_timezone())
|
||||
event_structure["modified_date"] = d
|
||||
@ -1163,7 +1363,13 @@ class Event(models.Model):
|
||||
|
||||
result = Event(**event_structure)
|
||||
result.add_pending_organisers(organisers)
|
||||
|
||||
if email or comments:
|
||||
has_comments = not comments in ["", None]
|
||||
result.set_message(Message(subject=_('during import process'),
|
||||
email=email,
|
||||
message=comments,
|
||||
closed=False,
|
||||
message_type=Message.TYPE.FROM_CONTRIBUTOR if has_comments else Message.TYPE.FROM_CONTRIBUTOR_NO_MSG))
|
||||
return result
|
||||
|
||||
|
||||
@ -1315,9 +1521,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
|
||||
@ -1338,6 +1545,8 @@ class Event(models.Model):
|
||||
"category",
|
||||
"tags",
|
||||
]
|
||||
if not no_m2m:
|
||||
result += ["organisers"]
|
||||
|
||||
result += [
|
||||
"title",
|
||||
@ -1349,8 +1558,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:
|
||||
@ -1377,8 +1584,8 @@ class Event(models.Model):
|
||||
|
||||
def update(self, other, all):
|
||||
|
||||
# do not update if all is false
|
||||
if other.has_pending_organisers() and not all:
|
||||
# integrate pending organisers
|
||||
if other.has_pending_organisers():
|
||||
self.organisers.set(other.pending_organisers)
|
||||
|
||||
# set attributes
|
||||
@ -1397,12 +1604,19 @@ class Event(models.Model):
|
||||
self.uuids.append(uuid)
|
||||
|
||||
# add possible missing sources
|
||||
for source in other.import_sources:
|
||||
if source not in self.import_sources:
|
||||
self.import_sources.append(source)
|
||||
if other.import_sources:
|
||||
if not self.import_sources:
|
||||
self.import_sources = []
|
||||
for source in other.import_sources:
|
||||
if source not in self.import_sources:
|
||||
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 = []
|
||||
@ -1429,6 +1643,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)
|
||||
@ -1455,9 +1670,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.
|
||||
@ -1485,13 +1705,23 @@ class Event(models.Model):
|
||||
for e in to_import:
|
||||
if e.is_event_long_duration():
|
||||
e.status = Event.STATUS.DRAFT
|
||||
e.set_message(
|
||||
Message(subject=_('Import'),
|
||||
name=_('import process'),
|
||||
message=_("The duration of the event is a little too long for direct publication. Moderators can choose to publish it or not."),
|
||||
message_type=Message.TYPE.IMPORT_PROCESS)
|
||||
)
|
||||
|
||||
# then import all the new events
|
||||
imported = Event.objects.bulk_create(to_import)
|
||||
# update organisers (m2m relation)
|
||||
for i, ti in zip(imported, to_import):
|
||||
if ti.has_pending_organisers():
|
||||
if ti.has_pending_organisers() and ti.pending_organisers is not None:
|
||||
i.organisers.set(ti.pending_organisers)
|
||||
if ti.has_message():
|
||||
msg = ti.get_message()
|
||||
msg.related_event = i
|
||||
msg.save()
|
||||
|
||||
nb_updated = Event.objects.bulk_update(
|
||||
to_update,
|
||||
@ -1565,13 +1795,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)
|
||||
]
|
||||
|
||||
@ -1581,10 +1810,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:
|
||||
@ -1635,9 +1864,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)
|
||||
@ -1673,16 +1905,50 @@ class Event(models.Model):
|
||||
return [Event.get_count_modification(w) for w in when_list]
|
||||
|
||||
|
||||
class ContactMessage(models.Model):
|
||||
class Message(models.Model):
|
||||
|
||||
class TYPE(models.TextChoices):
|
||||
FROM_CONTRIBUTOR = "from_contributor", _("From contributor")
|
||||
IMPORT_PROCESS = "import_process", _("Import process")
|
||||
CONTACT_FORM = "contact_form", _("Contact form")
|
||||
EVENT_REPORT = "event_report", _("Event report")
|
||||
FROM_CONTRIBUTOR_NO_MSG = "from_contrib_no_msg", _("From contributor (without message)")
|
||||
|
||||
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"),
|
||||
help_text=_("The subject of your message"),
|
||||
max_length=512,
|
||||
)
|
||||
|
||||
related_event = models.ForeignKey(
|
||||
Event,
|
||||
verbose_name=_("Related event"),
|
||||
help_text=_("The message is associated with this event."),
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
verbose_name=_("Author of the message"),
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
verbose_name=_("Name"),
|
||||
help_text=_("Your name"),
|
||||
@ -1722,8 +1988,18 @@ class ContactMessage(models.Model):
|
||||
null=True,
|
||||
)
|
||||
|
||||
def nb_open_contactmessages():
|
||||
return ContactMessage.objects.filter(closed=False).count()
|
||||
message_type = models.CharField(
|
||||
verbose_name=_("Type"),
|
||||
max_length=20,
|
||||
choices=TYPE.choices,
|
||||
default=None, null=True
|
||||
)
|
||||
|
||||
def nb_open_messages():
|
||||
return Message.objects.filter(Q(closed=False)&Q(spam=False)&Q(message_type__in=[Message.TYPE.CONTACT_FORM, Message.TYPE.EVENT_REPORT, Message.TYPE.FROM_CONTRIBUTOR])).count()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("message", kwargs={"pk": self.pk})
|
||||
|
||||
|
||||
class RecurrentImport(models.Model):
|
||||
@ -1744,6 +2020,10 @@ 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')
|
||||
LARAYMONDE = "raymonde", _('La Raymonde')
|
||||
APIDAE = 'apidae', _('Agenda apidae tourisme')
|
||||
IGUANA = 'iguana', _('Agenda iguana (médiathèques)')
|
||||
|
||||
class DOWNLOADER(models.TextChoices):
|
||||
SIMPLE = "simple", _("simple")
|
||||
@ -1811,6 +2091,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"),
|
||||
@ -1871,6 +2157,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)
|
||||
|
||||
|
@ -30,6 +30,11 @@ else:
|
||||
","
|
||||
)
|
||||
|
||||
ADMINS = [tuple(a.split(',')) for a in os_getenv("ADMINS", "").split(";")]
|
||||
MANAGERS = [tuple(a.split(',')) for a in os_getenv("MANAGERS", "").split(";")]
|
||||
SERVER_EMAIL = os_getenv("SERVER_EMAIL", "")
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
@ -56,13 +61,15 @@ INSTALLED_APPS = [
|
||||
"robots",
|
||||
"debug_toolbar",
|
||||
"cache_cleaner",
|
||||
"honeypot",
|
||||
]
|
||||
|
||||
SITE_ID = 1
|
||||
HONEYPOT_FIELD_NAME = "alias_name"
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.middleware.common.BrokenLinkEmailsMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware", # CorsMiddleware should be placed as high as possible,
|
||||
@ -72,6 +79,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 +153,7 @@ TIME_ZONE = "Europe/Paris"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
USE_TZ = False
|
||||
|
||||
LANGUAGES = (
|
||||
("fr", _("French")),
|
||||
@ -248,6 +256,7 @@ LOCATION_FIELD = {
|
||||
# stop robots
|
||||
|
||||
ROBOTS_USE_SITEMAP = False
|
||||
ROBOTS_SITE_BY_REQUEST = 'cached-sitemap'
|
||||
|
||||
# debug
|
||||
if DEBUG:
|
||||
@ -267,6 +276,11 @@ LOGGING = {
|
||||
"class": "logging.FileHandler",
|
||||
"filename": "backend.log",
|
||||
},
|
||||
"mail_admins": {
|
||||
"level": "ERROR",
|
||||
"class": "django.utils.log.AdminEmailHandler",
|
||||
"include_html": True,
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
|
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 |
BIN
src/agenda_culturel/static/images/fb.png
Normal file
BIN
src/agenda_culturel/static/images/fb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 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%);
|
||||
}
|
||||
|
||||
|
||||
@ -147,7 +150,7 @@ details[role="list"] summary + ul li.selected>a:hover {
|
||||
}
|
||||
|
||||
}
|
||||
.suggested-tags {
|
||||
.suggestions {
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
@ -201,10 +204,16 @@ details[role="list"] summary + ul li.selected>a:hover {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.suggested-tags .small-cat {
|
||||
.suggestions .small-cat {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.small-location {
|
||||
@extend .small-cat;
|
||||
border-color: var(--contrast);
|
||||
color: var(--contrast);
|
||||
}
|
||||
|
||||
.circ-cat.circ-large {
|
||||
height: 2.6em;
|
||||
width: 2.6em;
|
||||
@ -289,15 +298,7 @@ svg {
|
||||
width: 100%;
|
||||
padding: 0.3em;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 550px) {
|
||||
.illustration {
|
||||
width: 40%;
|
||||
float: right;
|
||||
margin: 0 0 0.5em .5em;
|
||||
}
|
||||
|
||||
float: right;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
@ -323,6 +324,7 @@ footer [data-tooltip] {
|
||||
scroll-behavior: smooth;
|
||||
transition-duration: 200ms;
|
||||
|
||||
|
||||
.cat {
|
||||
margin-right: 0;
|
||||
}
|
||||
@ -495,6 +497,15 @@ body > main {
|
||||
padding-top: 0.2em;
|
||||
}
|
||||
|
||||
body.authenticated > main {
|
||||
padding-top: 0.8em;
|
||||
}
|
||||
@media only screen and (min-width: 700px) {
|
||||
body.authenticated > main {
|
||||
padding-top: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
margin: 1em 0;
|
||||
}
|
||||
@ -603,12 +614,24 @@ header .remarque {
|
||||
}
|
||||
|
||||
.form.recent, .form.main-filter, .search .form {
|
||||
#id_status>div {
|
||||
#id_status>div, #id_representative>div {
|
||||
display: inline-block;
|
||||
margin-right: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
#search {
|
||||
form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
column-gap: .5em;
|
||||
:first-child,
|
||||
button {
|
||||
grid-column: 1/3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Basic picocss alerts */
|
||||
|
||||
|
||||
@ -851,7 +874,7 @@ nav>div {
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
@media only screen and (min-width: 1400px) {
|
||||
.header li {
|
||||
float: left;
|
||||
}
|
||||
@ -893,6 +916,39 @@ nav>div {
|
||||
color: var(--secondary-inverse);
|
||||
}
|
||||
|
||||
#badges {
|
||||
position: absolute;
|
||||
font-size: 70%;
|
||||
top:3.5em;
|
||||
left: 0;
|
||||
padding: 0.2em .5em 0.2em 0.2em;
|
||||
background: var(--card-sectionning-background-color);
|
||||
display: inline-block;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
|
||||
.badge {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
|
||||
}
|
||||
.link {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 700px) {
|
||||
#badges {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translate(-50%, 0);
|
||||
padding: 0 .5em .2em .5em;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.tw-badge {
|
||||
background: black;
|
||||
border-color: black;
|
||||
@ -1400,17 +1456,28 @@ img.preview {
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.single-event, .tag-description {
|
||||
display: grid;
|
||||
grid-template-columns: 60% auto;
|
||||
grid-column-gap: 1em;
|
||||
}
|
||||
header {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
.resume {
|
||||
column-count: 4;
|
||||
}
|
||||
.single-event, .tag-description {
|
||||
display: grid;
|
||||
grid-template-columns: 30% auto 14em;
|
||||
grid-column-gap: 1em;
|
||||
|
||||
header {
|
||||
margin: 0;
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
.illustration {
|
||||
width: auto;
|
||||
@ -1428,26 +1495,50 @@ img.preview {
|
||||
}
|
||||
}
|
||||
|
||||
form.messages div, form.moderation-events {
|
||||
@media only screen and (min-width: 992px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
fieldset {
|
||||
.header-complement {
|
||||
float: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
.header-complement {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
label {
|
||||
clear: both;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
form.messages {
|
||||
div {
|
||||
width: 100%;
|
||||
display: block;
|
||||
fieldset div {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 800px) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
:last-child {
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
div fieldset div {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.moderate-preview .event-body {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#moderate-form #id_status {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
label.required::after {
|
||||
content: ' *';
|
||||
color: red;
|
||||
@ -1493,6 +1584,240 @@ label.required::after {
|
||||
}
|
||||
}
|
||||
|
||||
.maskable_group .body_group.closed {
|
||||
display: none;
|
||||
.maskable_group {
|
||||
margin: 0.5em 0;
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-socalmedia {
|
||||
height: 1.3em;
|
||||
vertical-align: middle;
|
||||
margin-bottom: .2em;
|
||||
}
|
@ -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 %}
|
@ -1,37 +0,0 @@
|
||||
{% extends "agenda_culturel/page-admin.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% block og_title %}Contact{% endblock %}{% endblock %}
|
||||
|
||||
{% block entete_header %}
|
||||
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
|
||||
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
|
||||
<script src="/static/admin/js/jquery.init.js"></script>
|
||||
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
|
||||
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
|
||||
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block fluid %}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Contact</h1>
|
||||
|
||||
<article>
|
||||
{% url 'contact' as local_url %}
|
||||
{% include "agenda_culturel/static_content.html" with name="contact" url_path=local_url %}
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<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>
|
||||
</header>
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Envoyer">
|
||||
</form>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
@ -1,52 +0,0 @@
|
||||
{% extends "agenda_culturel/page-admin.html" %}
|
||||
{% load static %}
|
||||
{% load utils_extra %}
|
||||
|
||||
{% block title %}{% block og_title %}Message de contact : {{ obj.subject }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block entete_header %}
|
||||
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
|
||||
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
|
||||
<script src="/static/admin/js/jquery.init.js"></script>
|
||||
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
|
||||
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
|
||||
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidemenu-bouton %}
|
||||
<li><a href="#contenu-principal" aria-label="Aller au contenu">{% picto_from_name "chevron-up" %}</a></li>
|
||||
<li><a href="#sidebar" aria-label="Aller au menu latéral">{% picto_from_name "chevron-down" %}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid two-columns">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<h1>Modération du message « {{ object.subject }} »</h1>
|
||||
<ul>
|
||||
<li>Date : {{ object.date }}</li>
|
||||
<li>Auteur : {{ object.name }} <a href="mailto:{{ object.email }}">{{ object.email }}</a></li>
|
||||
</ul>
|
||||
</header>
|
||||
<div>
|
||||
{{ object.message }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Enregistrer">
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
|
||||
</div>
|
||||
|
||||
{% 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 {% picto_from_name "edit-3" %}</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
|
@ -0,0 +1,9 @@
|
||||
Bonjour,
|
||||
|
||||
Nous avons le plaisir de t'informer que l'événement « {{ event_title }} » que tu as proposé sur {{ sitename }} a été validé et publié par l'équipe de modération.
|
||||
Tu peux dès maintenant le retrouver à l'adresse suivante :
|
||||
- {{ url }}
|
||||
|
||||
Merci de participer à l'amélioration de {{ sitename }}. N'hésites pas à continuer à contribuer en ajoutant de nouveaux événements, ça nous fait bien plaisir.
|
||||
|
||||
L'équipe de modération.
|
@ -0,0 +1,8 @@
|
||||
Bonjour,
|
||||
|
||||
Nous avons la dure tâche de t'informer que l'événement « {{ event_title }} » que tu avais proposé sur {{ sitename }} n'a pas été retenu par l'équipe de modération.
|
||||
|
||||
Nous te remercions pour cette proposition, et espérons qu'une prochaine fois, ta proposition correspondra à la ligne portée par {{ sitename }}.
|
||||
|
||||
L'équipe de modération.
|
||||
|
@ -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) :
|
||||
|
@ -1,14 +1,24 @@
|
||||
{% load utils_extra %}
|
||||
{% load static %}
|
||||
|
||||
{% with event.get_reference_urls as refs %}
|
||||
{% if refs|length > 0 %}
|
||||
<p>Source{{ refs|pluralize }} :
|
||||
|
||||
{% for eurl in refs %}
|
||||
<a href="{{ eurl }}">{{ eurl|hostname }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% with refs|length as nb_url %}
|
||||
{% if nb_url > 0 %}
|
||||
{% if nb_url == 1 %}
|
||||
{% if refs.0|is_facebook_url %}
|
||||
<p>Voir <a href="{{ refs.0 }}">l'événement facebook <img class="logo-socalmedia" src="{% static 'images/fb.png' %}" /></a></p>
|
||||
{% else %}
|
||||
<p>Voir l'événement sur le site source <a href="{{ refs.0 }}">{{ refs.0|hostname }}</a></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>Voir l'événement sur les site sources
|
||||
{% for eurl in refs %}
|
||||
|
||||
<a href="{{ eurl }}">{{ eurl|hostname }}{% if eurl|is_facebook_url %} <img class="logo-socalmedia" src="{% static 'images/fb.png' %}" />{% endif %}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p><em>À notre connaissance, cet événement n'est pas référencé autre part sur internet.</em></p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
@ -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>
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load tag_extra %}
|
||||
{% load utils_extra %}
|
||||
{% load static %}
|
||||
{% load locations_extra %}
|
||||
|
||||
{% if noarticle == 0 %}
|
||||
<article id="filters">
|
||||
@ -65,8 +66,10 @@
|
||||
<button type="submit">Appliquer le filtre</button>
|
||||
</form>
|
||||
</details>
|
||||
<div class="suggested-tags">
|
||||
{% show_suggested_tags filter=filter %}
|
||||
<div class="suggestions">
|
||||
Suggestion :
|
||||
{% show_suggested_positions filter=filter %}
|
||||
{% show_suggested_tags filter=filter %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
|
@ -6,7 +6,13 @@
|
||||
{% for group, fields in form.fields_by_group %}
|
||||
<div {% if group.maskable %}class="maskable_group"{% endif %} id="group_{{ group.id }}">
|
||||
{% if group.maskable %}
|
||||
<input class="toggle_body" type="checkbox" id="maskable_group_{{ group.id }}" name="group_{{ group.id }}"><label for="maskable_group_{{ group.id }}">{{ group.label }}</label>
|
||||
<input
|
||||
class="toggle_body"
|
||||
type="checkbox"
|
||||
id="maskable_group_{{ group.id }}"
|
||||
name="group_{{ group.id }}"
|
||||
{% if not group.default_masked %}checked{% endif %}
|
||||
><label for="maskable_group_{{ group.id }}">{{ group.label }}</label>
|
||||
{% endif %}
|
||||
<div class="error_group">
|
||||
{% for field in fields %}
|
||||
@ -36,7 +42,9 @@
|
||||
<script>
|
||||
const maskables = document.querySelectorAll('.maskable_group');
|
||||
maskables.forEach(function (item) {
|
||||
item.querySelector('.body_group').classList.add('closed');
|
||||
if (!item.checked) {
|
||||
item.querySelector('.body_group').classList.add('closed');
|
||||
}
|
||||
console.log('item ' + item);
|
||||
|
||||
item.querySelector('.toggle_body').addEventListener('change', (event) => {
|
||||
|
@ -21,7 +21,7 @@
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
{{ form.media }}
|
||||
{{ form.as_p }}
|
||||
{{ form }}
|
||||
<input type="submit" value="Lancer l'import" id="import-button">
|
||||
</form>
|
||||
<p>Si tu as plein d'événements à ajouter, tu peux les <a href="{% url 'add_event_urls' %}" >ajouter par lots</a>.</p>
|
||||
|
@ -28,6 +28,12 @@
|
||||
|
||||
{{ formset.management_form }}
|
||||
{% csrf_token %}
|
||||
{% if contactform %}
|
||||
<article>
|
||||
{{ contactform }}
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
{% for form in formset %}
|
||||
<article>
|
||||
<header>
|
||||
|
@ -0,0 +1,51 @@
|
||||
{% 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 %}
|
||||
|
||||
{% block entete_header %}
|
||||
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
|
||||
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
|
||||
<script src="/static/admin/js/jquery.init.js"></script>
|
||||
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
|
||||
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
|
||||
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block fluid %}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h1>{% if form.event %}Contact au sujet de l'événement « {{ form.event.title }} »{% else %}
|
||||
Contact{% endif %}</h1>
|
||||
|
||||
{% if not form.event %}
|
||||
<article>
|
||||
{% url 'contact' as local_url %}
|
||||
{% include "agenda_culturel/static_content.html" with name="contact" url_path=local_url %}
|
||||
</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>
|
||||
{% 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
|
||||
afin que l'on puisse répondre à tes demandes ou remarques.
|
||||
</p>
|
||||
{% endif %}
|
||||
</header>
|
||||
<form method="post">{% csrf_token %}
|
||||
{% render_honeypot_field "alias_name" %}
|
||||
{{ form.media }}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Envoyer">
|
||||
</form>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,63 @@
|
||||
{% extends "agenda_culturel/page-admin.html" %}
|
||||
{% load static %}
|
||||
{% load utils_extra %}
|
||||
|
||||
{% block title %}{% block og_title %}Message {{ obj.subject }}{% endblock %}{% endblock %}
|
||||
|
||||
{% block entete_header %}
|
||||
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
|
||||
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
|
||||
<script src="/static/admin/js/jquery.init.js"></script>
|
||||
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
|
||||
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
|
||||
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidemenu-bouton %}
|
||||
<li><a href="#contenu-principal" aria-label="Aller au contenu">{% picto_from_name "chevron-up" %}</a></li>
|
||||
<li><a href="#sidebar" aria-label="Aller au menu latéral">{% picto_from_name "chevron-down" %}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid two-columns">
|
||||
<div>
|
||||
<article>
|
||||
<header>
|
||||
<div class="slide-buttons">
|
||||
<a href="{% url 'delete_message' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
|
||||
</div>
|
||||
|
||||
<h1>Message « {{ object.subject }} »</h1>
|
||||
<ul>
|
||||
<li>Date : {{ object.date.date }} à {{ object.date.time }}</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 %}
|
||||
<li>Type : {% if object.message_type %}{{ object.get_message_type_display }}{% else %}-{% endif %}</li>
|
||||
</ul>
|
||||
</header>
|
||||
<div>
|
||||
{{ object.message | safe }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
{% if object.message_type == "from_contributor" or object.message_type == "from_contrib_no_msg" %}<p class="message info">Ce message a été envoyé par une personne lors
|
||||
de l'ajout d'un événement.
|
||||
{% if object.closed %}
|
||||
En décochant fermé, vous modifiez manuellement son statut, et cela pourra entraîner l'envoi d'un message de notification
|
||||
lors de la modification future de l'événement associé.{% else %}
|
||||
En cochant fermé, vous modifiez manuellement son statut, et cela empêchera l'envoi d'un message de notification
|
||||
lors de la modification future de l'événement associé.
|
||||
{% endif %}
|
||||
</p>{% endif %}
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Enregistrer">
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{% include "agenda_culturel/side-nav.html" with current="messages" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -34,8 +34,10 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>Sujet</th>
|
||||
<th>Auteur</th>
|
||||
<th>Événement</th>
|
||||
<th>Fermé</th>
|
||||
<th>Spam</th>
|
||||
</tr>
|
||||
@ -44,8 +46,10 @@
|
||||
{% 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>{% if obj.message_type %}{{ obj.get_message_type_display }}{% else %}-{% endif %}</td>
|
||||
<td><a href="{% url 'message' obj.pk %}">{{ obj.subject }}</a></td>
|
||||
<td>{% if obj.user %}<em>{{ obj.user }}</em>{% else %}{% if obj.name %}{{ obj.name }}{% else %}-{% endif %}{% 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>
|
||||
</tr>
|
||||
@ -57,7 +61,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,62 @@
|
||||
|
||||
{% 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{% if not step.data.closed %} (ouvert){% endif %}</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 {% if step.data.name %}{{ step.data.name }}{% if step.data.email %} (<a href="mailto: {{ step.data.email }}">{{ step.data.email }}</a>){% endif %}{% else %} <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 +93,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 %}
|
||||
@ -122,12 +132,15 @@
|
||||
{% else %}
|
||||
Signaler comme doublon
|
||||
{% endif %}</a>
|
||||
</article>
|
||||
<a role="button" href="{% url 'message_for_event' event.pk %}">Signaler cet événement</a>
|
||||
</article>
|
||||
|
||||
|
||||
</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>
|
||||
@ -107,7 +107,7 @@
|
||||
{% if event.start_day == day.date and event.start_time %}
|
||||
{{ event.start_time }}
|
||||
{% endif %}
|
||||
<a href="{{ event.get_absolute_url }}">{{ event|picto_status }} {{ event.title }} {{ event|tw_badge }}</a>
|
||||
<a href="{{ event.get_absolute_url }}">{{ event|picto_status }} {{ event.title|no_emoji }} {{ event|tw_badge }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</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>
|
||||
|
@ -39,13 +39,13 @@
|
||||
<li><strong>Adresse naviguable :</strong> <a href="{{ object.browsable_url }}">{{ object.browsable_url }}</a></li>
|
||||
<li><strong>Valeurs par défaut :</strong>
|
||||
<ul>
|
||||
<li><strong>Publié :</strong> {{ object.defaultPublished }}</li>
|
||||
<li><strong>Localisation :</strong> {{ object.defaultLocation }}</li>
|
||||
<li><strong>Publié :</strong> {{ object.defaultPublished|yesno:"Oui,Non" }}</li>
|
||||
{% 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>
|
||||
<li><strong>Organisateur :</strong> <a href="{{ object.defaultOrganiser.get_absolute_url }}">{{ object.defaultOrganiser }}</a></li>
|
||||
{% if object.defaultOrganiser %}<li><strong>Organisateur :</strong> <a href="{{ object.defaultOrganiser.get_absolute_url }}">{{ object.defaultOrganiser }}</a></li>{% endif %}
|
||||
<li><strong>Étiquettes :</strong>
|
||||
{% for tag in object.defaultTags %}
|
||||
{{ tag|tw_highlight }}{% if not forloop.last %}, {% endif %}
|
||||
<a href="{% url 'view_tag' tag %}">{{ tag|tw_highlight }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -95,13 +95,19 @@
|
||||
<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 }}
|
||||
{% endif %}
|
||||
{{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title }}</a> {{ event|tw_badge }}
|
||||
{{ 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 %}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<script src="{% static 'js/calendar-buttons.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% block og_title %}Semaine du {{ calendar.firstdate|date|frdate }}{% endblock %}{% endblock %}
|
||||
{% block title %}{% block og_title %}{% if calendar.today_in_calendar %}Sorties culturelles cette semaine à Clermont-Ferrand et aux environs{% else %}Semaine du {{ calendar.firstdate|date|frdate }}{% endif %}{% endblock %}{% endblock %}
|
||||
|
||||
{% block ce_mois_ci_parameters %}{% block cette_semaine_parameters %}{% block a_venir_parameters %}?{{ filter.get_url }}{% endblock %}{% endblock %}{% endblock %}
|
||||
|
||||
@ -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,20 +80,28 @@
|
||||
</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 }}
|
||||
{% endif %}
|
||||
{{ event|picto_status }} <a href="{{ event.get_absolute_url }}" data-target="event-{{ event.id }}" onClick="toggleModal(event)">{{ event.title }}</a>
|
||||
{{ event|picto_status }} <a href="{{ event.get_absolute_url }}" data-target="event-{{ event.id }}" onClick="toggleModal(event)">{{ event.title|no_emoji }}</a>
|
||||
{{ 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,15 +1,23 @@
|
||||
<!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">
|
||||
<title>Pommes de lune — {% block title %}{% endblock %}</title>
|
||||
<meta name="google-site-verification" content="pvRD0rc_xIE-1IYmbao0kj5ngGo1IWxJqKwoxrQwxuA" />
|
||||
<meta name="keywords" content="Clermont-Ferrand, Puy-de-Dôme, agenda culturel, agenda participatif, sortir à clermont, sorties, concerts, théâtre, danse, animations, ateliers, lectures">
|
||||
|
||||
{% 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:description" content="{% block og_description %}Où sortir à Clermont-Ferrand? Retrouve tous les bons plans sur l'agenda participatif des événements culturels à Clermont-Ferrand et dans le Puy-de-Dôme{% endblock %}" />
|
||||
<meta property="og:image" content="{% block og_image %}https://{{ request.get_host }}{% get_media_prefix %}screenshot.png{% endblock %}" />
|
||||
<meta property="og:locale" content="fr_FR" />
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}" />
|
||||
|
||||
{% if debug %}
|
||||
@ -27,13 +35,8 @@
|
||||
{% 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 %}">
|
||||
|
||||
<body class="{% block body-class %}contenu{% endblock %} {% if user.is_authenticated %}authenticated{% endif %}">
|
||||
<div id="boutons-fixes">
|
||||
<ul>
|
||||
{% block sidemenu-bouton %}{% endblock %}
|
||||
@ -47,10 +50,9 @@
|
||||
<input class="menu-btn" type="checkbox" id="menu-btn" />
|
||||
<label class="menu-icon" for="menu-btn">{% picto_from_name "menu" %}</label>
|
||||
<ul class="menu">
|
||||
{% 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>
|
||||
@ -64,30 +66,36 @@
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
{% if perms.agenda_culturel.view_recurrentimport %}
|
||||
{% show_badges_rimports "bottom" %}
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.change_event %}
|
||||
{% show_badges_events "bottom" %}
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.change_duplicatedevents %}
|
||||
{% show_badge_duplicated "bottom" %}
|
||||
{% endif %}
|
||||
{% 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" %}
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
{{ user.username }} @
|
||||
{% endif %}
|
||||
<a href="{% url 'home' %}" aria-label="Retour accueil">Pommes de lune</a>
|
||||
</div>
|
||||
<div class="soustitre">Événements culturels à Clermont-Ferrand et aux environs</div>
|
||||
<div class="soustitre">Agenda participatif des sorties culturelles à Clermont-Ferrand et aux environs</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% if user.is_authenticated %}
|
||||
<div id="badges">
|
||||
{% if perms.agenda_culturel.view_recurrentimport %}
|
||||
{% show_badges_rimports "bottom" %}
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.change_event %}
|
||||
{% show_badges_events "bottom" %}
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.change_duplicatedevents %}
|
||||
{% show_badge_duplicated "bottom" %}
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %}
|
||||
{% show_badge_unknown_places "bottom" %}
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.view_message %}
|
||||
{% show_badge_messages "bottom" %}
|
||||
{% endif %}
|
||||
{% show_badge_moderate %}
|
||||
<a class="link" href="{% url 'administration' %}">Administrer {% picto_from_name "settings" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<main class="container{% block fluid %}-fluid{% endblock %}">
|
||||
{% if messages %}
|
||||
|
@ -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">
|
||||
|
@ -6,6 +6,8 @@
|
||||
{% block configurer-bouton %}{% endblock %}
|
||||
|
||||
{% block entete_header %}
|
||||
<script src="{% static 'choicejs/choices.min.js' %}"></script>
|
||||
|
||||
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
|
||||
<script src="/static/admin/js/jquery.init.js"></script>
|
||||
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
|
||||
@ -26,4 +28,35 @@
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<script>
|
||||
show_firstgroup = {
|
||||
choice(classes, choice) {
|
||||
const i = Choices.defaults.templates.choice.call(this, classes, choice);
|
||||
if (this.first_group !== null && choice.groupId == this.first_group)
|
||||
i.classList.add("visible");
|
||||
return i;
|
||||
},
|
||||
choiceGroup(classes, group) {
|
||||
const g = Choices.defaults.templates.choiceGroup.call(this, classes, group);
|
||||
if (this.first_group === undefined && group.value == "Suggestions")
|
||||
this.first_group = group.id;
|
||||
if (this.first_group !== null && group.id == this.first_group)
|
||||
g.classList.add("visible");
|
||||
return g;
|
||||
}
|
||||
};
|
||||
|
||||
const tags = document.querySelector('#id_defaultTags');
|
||||
const choices_tags = new Choices(tags,
|
||||
{
|
||||
placeholderValue: 'Sélectionner les étiquettes par défaut',
|
||||
allowHTML: true,
|
||||
delimiter: ',',
|
||||
removeItemButton: true,
|
||||
shouldSort: false,
|
||||
callbackOnCreateTemplates: () => (show_firstgroup)
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@ -16,13 +16,13 @@
|
||||
{% block content %}
|
||||
|
||||
|
||||
<article class="search">
|
||||
<article class="search" id="search">
|
||||
<header>
|
||||
<h1>Rechercher un événement</h1>
|
||||
</header>
|
||||
|
||||
<form method="get" class="form django-form">
|
||||
{{ filter.form }}
|
||||
{{ filter.form.as_div }}
|
||||
<button type="submit">Rechercher</button>
|
||||
|
||||
{% if full %}
|
||||
@ -36,7 +36,7 @@
|
||||
{% if has_results or categories %}
|
||||
<div id="results">
|
||||
{% if categories %}
|
||||
<div class="message info">
|
||||
<div class="message success">
|
||||
{% if categories.count > 1 %}
|
||||
Retrouvez les événements correspondant aux catégories
|
||||
{% else %}
|
||||
@ -49,7 +49,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if tags %}
|
||||
<div class="message info">
|
||||
<div class="message success">
|
||||
{% if tags.count > 1 %}
|
||||
Retrouvez les événements correspondant aux étiquettes
|
||||
{% else %}
|
||||
@ -62,7 +62,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if places %}
|
||||
<div class="message info">
|
||||
<div class="message success">
|
||||
{% if places.count > 1 %}
|
||||
Retrouvez les événements se déroulant dans les lieux
|
||||
{% else %}
|
||||
@ -75,7 +75,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if organisations %}
|
||||
<div class="message info">
|
||||
<div class="message success">
|
||||
{% if organisations.count > 1 %}
|
||||
Retrouvez les événements correspondant aux organisateurs
|
||||
{% else %}
|
||||
@ -88,7 +88,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if rimports and user.is_authenticated %}
|
||||
<div class="message info">
|
||||
<div class="message success">
|
||||
{% if rimports.count > 1 %}
|
||||
Import récurrent correspondant à la recherche :
|
||||
{% else %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% load event_extra %}
|
||||
{% load contactmessages_extra %}
|
||||
{% load messages_extra %}
|
||||
{% load duplicated_extra %}
|
||||
{% load utils_extra %}
|
||||
<aside id="sidebar">
|
||||
@ -16,7 +16,7 @@
|
||||
<nav>
|
||||
<ul>
|
||||
{% if perms.agenda_culturel.change_event %}
|
||||
<li><a {% if current == "recent" %}class="selected" {% endif %}href="{% url 'recent' %}">Derniers événements soumis</a>{% show_badges_events "left" %}</li>
|
||||
<li><a {% if current == "recent" %}class="selected" {% endif %}href="{% url 'recent' %}">Derniers événements ajoutés</a>{% show_badges_events "left" %}</li>
|
||||
{% endif %}
|
||||
{% if perms.agenda_culturel.change_duplicatedevents %}
|
||||
<li><a {% if current == "duplicates" %}class="selected" {% endif %}href="{% url 'duplicates' %}">Gestion des doublons</a>{% show_badge_duplicated "left" %}</li>
|
||||
@ -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">
|
||||
|
@ -4,6 +4,36 @@
|
||||
{% load event_extra %}
|
||||
{% load tag_extra %}
|
||||
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
{% if event.other_versions %}
|
||||
{% with poss_dup=event.get_other_versions %}
|
||||
{% if poss_dup|length > 0 and event.masked %}
|
||||
<div class="message warning">
|
||||
{% if event.other_versions.representative %}
|
||||
{% if event.pure_import %}Cette version de l'événement est fidèle à la source, mais l'événement existe en{% else %}
|
||||
Cette version de l'événement existe en
|
||||
{% endif %}
|
||||
<a href="{{ event.other_versions.get_absolute_url }}">en plusieurs versions</a>.<br>
|
||||
Tu peux consulter <a href="{{ event.other_versions.get_one_event.get_absolute_url }}">la version mise en avant</a>.
|
||||
{% else %}
|
||||
Cet événement existe <a href="{{ event.other_versions.get_absolute_url }}">en plusieurs versions</a>, et aucune n'a encore été choisie pour être mise en avant.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if event.get_other_not_trash_versions|length == 0 %}<p class="remarque">Aucune autre version n'est accessible publiquement</p>{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if event.other_versions.representative and event.masked %}
|
||||
<div class="message warning">
|
||||
Cette version de l'événement n'est pas dans sa forme la plus avantageuse.
|
||||
L'équipe de modération a préparé pour toi une <a href="{{ event.other_versions.representative.get_absolute_url }}">version à jour</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<article>
|
||||
<header>
|
||||
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
|
||||
@ -22,16 +52,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>
|
||||
@ -47,33 +78,23 @@
|
||||
{% endwith %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
{% if event.other_versions %}
|
||||
{% with poss_dup=event.get_other_versions %}
|
||||
{% if poss_dup|length > 0 and event.other_versions.representative and not event.masked %}
|
||||
<p class="remarque">Cette version est celle mise en avant, cependant il en existe <a href="{{ event.other_versions.get_absolute_url }}">en plusieurs versions</a>.</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if event.other_versions %}
|
||||
{% with poss_dup=event.get_other_versions %}
|
||||
{% if poss_dup|length > 0 %}
|
||||
<p class="remarque">
|
||||
{% if event.other_versions.representative %}
|
||||
Cet événement existe <a href="{{ event.other_versions.get_absolute_url }}">en plusieurs versions</a>,
|
||||
{% if event.masked %}
|
||||
vous pouvez consulter <a href="{{ event.other_versions.get_one_event.get_absolute_url }}">la version mise en avant</a>
|
||||
{% else %}
|
||||
et vous consultez la version mise en avant.
|
||||
{% endif %}
|
||||
{% else %}
|
||||
cet événement existe probablement <a href="{{ event.other_versions.get_absolute_url }}">en plusieurs versions</a>.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if event.get_other_not_trash_versions|length == 0 %}<p class="remarque">Aucune autre version n'est accessible publiquement</p>{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if event.other_versions.representative and event.masked %}
|
||||
<p class="remarque">
|
||||
Vous consultez l'événement dans une version non consolidée. Nous vous invitons
|
||||
à consulter sa <a href="{{ event.other_versions.representative.get_absolute_url }}">version représentative</a>.
|
||||
{% if perms.agenda_culturel.change_message %}
|
||||
{% if event.message_set.all.count > 0 %}
|
||||
<p class="remarque">Cet événement a été l'objet {% if event.message_set.all.count == 1 %}d'un message{% else %}de messages{% 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>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
@ -124,9 +145,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">modifier {% picto_from_name "edit-3" %}</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>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% load tag_extra %}
|
||||
|
||||
{% block title %}{% block og_title %}Les événements {{ tag }}{% endblock %}{% endblock %}
|
||||
{% block title %}{% block og_title %}Étiquette {{ tag }}{% endblock %}{% endblock %}
|
||||
|
||||
{% load cat_extra %}
|
||||
{% load utils_extra %}
|
||||
@ -40,7 +40,7 @@
|
||||
<a href="{% url 'delete_tag' tag %}" role="button">Supprimer {% picto_from_name "trash-2" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Les événements <em>{{ tag }}</em></h1>
|
||||
<h1>Étiquette <em>{{ tag }}</em></h1>
|
||||
{% if user.is_authenticated and object %}
|
||||
{% if object.in_excluded_suggestions %}
|
||||
<p class="remarque">Cette étiquette fait partie des étiquettes suggérées pour l'exclusion.</p>
|
||||
@ -62,10 +62,28 @@
|
||||
{% endif %}
|
||||
|
||||
<footer>
|
||||
{% if user.is_authenticated and rimports %}
|
||||
<p>Cette étiquette est ajoutée par défaut {% if rimports.count == 1 %}à l'import récurrent{% else %}aux imports récurrents :{% endif %}
|
||||
{% for ri in rimports %}
|
||||
<a href="{{ ri.get_absolute_url }}">{{ ri.name }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% include 'agenda_culturel/paginator.html' %}
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<div>
|
||||
<div class="slide-buttons">
|
||||
{% if past %}
|
||||
<a href="{% url 'view_tag' tag|prepare_tag %}" role="button">Voir les événements à venir</a>
|
||||
{% else %}
|
||||
<a href="{% url 'view_tag_past' tag|prepare_tag %}" role="button">Voir les événements passés</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2>{% if past %}Événements passés{% else %}Événements à venir{% endif %}</h2>
|
||||
<div style="clear:both"></div>
|
||||
</div>
|
||||
{% for event in paginator_filter %}
|
||||
{% include "agenda_culturel/single-event/event-elegant-inc.html" with event=event day=0 %}
|
||||
{% endfor %}
|
||||
|
@ -20,6 +20,12 @@
|
||||
{% else %} sera bien sûr conservé, mais perdra cette étiquette.
|
||||
{% endif %}</p>
|
||||
{% endif %}
|
||||
{% if nbi > 0 %}
|
||||
<p>Remarquez qu'elle est associée à {{ nbi }} import{{ nbs|pluralize }} récurrent{{ nbs|pluralize }}, qui
|
||||
{% if nbi > 1 %} seront bien sûr conservés, mais perdront cette étiquette.
|
||||
{% else %} sera bien sûr conservé, mais perdra cette étiquette.
|
||||
{% endif %}</p>
|
||||
{% endif %}
|
||||
{% if obj %}
|
||||
<p>Différentes informations sont associées à cette étiquette (description, suggestion d'inclusion, etc)
|
||||
seront également perdues lors de cette suppression.</p>
|
||||
|
@ -8,7 +8,7 @@
|
||||
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{% block og_title %}Renseignement de l'étiquette {{ form.name.value }}{% endblock %}{% endblock %}
|
||||
{% block title %}{% block og_title %}{% if form.name.value != None %}Renseignement de l'étiquette {{ form.name.value }}{% else %}Création d'une étiquette{% endif %}{% endblock %}{% endblock %}
|
||||
|
||||
{% block fluid %}{% endblock %}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h1>Renseignement de l'étiquette <em>{{ form.name.value }}</em></h1>
|
||||
<h1>{% if form.name.value != None %}Renseignement de l'étiquette <em>{{ form.name.value }}</em>{% else %}Création d'une étiquette{% endif %}</h1>
|
||||
</header>
|
||||
|
||||
<form method="post">{% csrf_token %}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user