Merge branch 'main' into correction-script-import-comedie

This commit is contained in:
jmtrivial 2024-12-27 15:42:19 +01:00
commit cc0ae8b582
65 changed files with 2773 additions and 673 deletions

View File

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

View File

@ -5,10 +5,11 @@ WORKDIR /usr/src/app
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && \
apt-get install --no-install-recommends -y build-essential libpq-dev gettext chromium-driver gdal-bin \
apt-get install --no-install-recommends -y build-essential libpq-dev gettext chromium-driver gdal-bin fonts-symbola \
&& rm -rf /var/lib/apt/lists/*
COPY src/requirements.txt ./requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \

43
experimentations/get_le_rio.py Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/python3
# coding: utf-8
import os
import json
import sys
# getting the name of the directory
# where the this file is present.
current = os.path.dirname(os.path.realpath(__file__))
# Getting the parent directory name
# where the current directory is present.
parent = os.path.dirname(current)
# adding the parent directory to
# the sys.path.
sys.path.append(parent)
from src.agenda_culturel.import_tasks.downloader import *
from src.agenda_culturel.import_tasks.extractor import *
from src.agenda_culturel.import_tasks.importer import *
from src.agenda_culturel.import_tasks.custom_extractors import *
if __name__ == "__main__":
u2e = URL2Events(SimpleDownloader(), lerio.CExtractor())
url = "https://www.cinemalerio.com/evenements/"
url_human = "https://www.cinemalerio.com/evenements/"
try:
events = u2e.process(url, url_human, cache = "cache-le-rio.html", default_values = {"location": "Cinéma le Rio", "category": "Cinéma"}, published = True)
exportfile = "events-le-roi.json"
print("Saving events to file {}".format(exportfile))
with open(exportfile, "w") as f:
json.dump(events, f, indent=4, default=str)
except Exception as e:
print("Exception: " + str(e))

View File

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

View File

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

View File

@ -117,6 +117,23 @@ class DayInCalendar:
if e.start_time is None
else e.start_time
)
self.today_night = False
if self.is_today():
self.today_night = True
now = timezone.now()
nday = now.date()
ntime = now.time()
found = False
for idx,e in enumerate(self.events):
if (nday < e.start_day) or (nday == e.start_day and e.start_time and ntime <= e.start_time):
self.events[idx].is_first_after_now = True
found = True
break
if found:
self.today_night = False
def is_today_after_events(self):
return self.is_today() and self.today_night
def events_by_category_ordered(self):
from .models import Category
@ -175,12 +192,13 @@ class IntervalInDay(DayInCalendar):
self.id = self.id + '-' + str(id)
class CalendarList:
def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=None):
def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=None, qs=None):
self.firstdate = firstdate
self.lastdate = lastdate
self.now = date.today()
self.filter = filter
self.ignore_dup = ignore_dup
self.qs = qs
if exact:
self.c_firstdate = self.firstdate
@ -219,9 +237,12 @@ class CalendarList:
def fill_calendar_days(self):
if self.filter is None:
from .models import Event
if self.qs is None:
from .models import Event
qs = Event.objects.all()
qs = Event.objects.all()
else:
qs = self.qs
else:
qs = self.filter.qs
@ -229,7 +250,7 @@ class CalendarList:
qs = qs.exclude(other_versions=self.ignore_dup)
startdatetime = timezone.make_aware(datetime.combine(self.c_firstdate, time.min), timezone.get_default_timezone())
lastdatetime = timezone.make_aware(datetime.combine(self.c_lastdate, time.max), timezone.get_default_timezone())
self.events = qs.filter(
qs = qs.filter(
(Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime))
| (
Q(recurrence_dtend__isnull=False)
@ -243,7 +264,10 @@ class CalendarList:
Q(other_versions__isnull=True) |
Q(other_versions__representative=F('pk')) |
Q(other_versions__representative__isnull=True)
).order_by("start_time", "title__unaccent__lower").select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative")
).order_by("start_time", "title__unaccent__lower")
qs = qs.select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative")
self.events = qs
firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
@ -291,14 +315,14 @@ class CalendarList:
def time_intervals_list_first(self):
return self.time_intervals_list(True)
def export_to_ics(self):
def export_to_ics(self, request):
from .models import Event
events = [event for day in self.get_calendar_days().values() for event in day.events]
return Event.export_to_ics(events)
return Event.export_to_ics(events, request)
class CalendarMonth(CalendarList):
def __init__(self, year, month, filter):
def __init__(self, year, month, filter, qs=None):
self.year = year
self.month = month
r = calendar.monthrange(year, month)
@ -306,7 +330,7 @@ class CalendarMonth(CalendarList):
first = date(year, month, 1)
last = date(year, month, r[1])
super().__init__(first, last, filter)
super().__init__(first, last, filter, qs)
def get_month_name(self):
return self.firstdate.strftime("%B")
@ -319,14 +343,14 @@ class CalendarMonth(CalendarList):
class CalendarWeek(CalendarList):
def __init__(self, year, week, filter):
def __init__(self, year, week, filter, qs=None):
self.year = year
self.week = week
first = date.fromisocalendar(self.year, self.week, 1)
last = date.fromisocalendar(self.year, self.week, 7)
super().__init__(first, last, filter)
super().__init__(first, last, filter, qs)
def next_week(self):
return self.firstdate + timedelta(days=7)
@ -336,8 +360,8 @@ class CalendarWeek(CalendarList):
class CalendarDay(CalendarList):
def __init__(self, date, filter=None):
super().__init__(date, date, filter, exact=True)
def __init__(self, date, filter=None, qs=None):
super().__init__(date, date, filter=filter, qs=qs, exact=True)
def get_events(self):
return self.calendar_days_list()[0].events

View File

@ -6,7 +6,8 @@ from celery.schedules import crontab
from celery.utils.log import get_task_logger
from celery.exceptions import MaxRetriesExceededError
import time as time_
from django.conf import settings
from celery.signals import worker_ready
from contextlib import contextmanager
@ -147,6 +148,8 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
extractor = c3c.CExtractor()
elif rimport.processor == RecurrentImport.PROCESSOR.ARACHNEE:
extractor = arachnee.CExtractor()
elif rimport.processor == RecurrentImport.PROCESSOR.LERIO:
extractor = lerio.CExtractor()
else:
extractor = None
@ -173,6 +176,11 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
published=published,
)
# force location if required
if rimport.forceLocation and location:
for i, e in enumerate(events['events']):
events['events'][i]["location"] = location
# convert it to json
json_events = json.dumps(events, default=str)
@ -248,6 +256,23 @@ def daily_imports(self):
run_recurrent_imports_from_list([imp.pk for imp in imports])
SCREENSHOT_FILE = settings.MEDIA_ROOT + '/screenshot.png'
@app.task(bind=True)
def screenshot(self):
downloader = ChromiumHeadlessDownloader(noimage=False)
downloader.screenshot("https://pommesdelune.fr", SCREENSHOT_FILE)
@worker_ready.connect
def at_start(sender, **k):
if not os.path.isfile(SCREENSHOT_FILE):
logger.info("Init screenshot file")
with sender.app.connection() as conn:
sender.app.send_task('agenda_culturel.celery.screenshot', None, connection=conn)
else:
logger.info("Screenshot file already exists")
@app.task(bind=True)
def run_all_recurrent_imports(self):
from agenda_culturel.models import RecurrentImport
@ -289,7 +314,7 @@ def weekly_imports(self):
run_recurrent_imports_from_list([imp.pk for imp in imports])
@app.task(base=ChromiumTask, bind=True)
def import_events_from_url(self, url, cat, tags, force=False):
def import_events_from_url(self, url, cat, tags, force=False, user_id=None):
from .db_importer import DBImporterEvents
from agenda_culturel.models import RecurrentImport, BatchImportation
from agenda_culturel.models import Event, Category
@ -298,7 +323,7 @@ def import_events_from_url(self, url, cat, tags, force=False):
if acquired:
logger.info("URL import: {}".format(self.request.id))
logger.info("URL import: {}".format(self.request.id) + " force " + str(force))
# clean url
@ -335,7 +360,7 @@ def import_events_from_url(self, url, cat, tags, force=False):
json_events = json.dumps(events, default=str)
# import events (from json)
success, error_message = importer.import_events(json_events)
success, error_message = importer.import_events(json_events, user_id)
# finally, close task
close_import_task(self.request.id, success, error_message, importer)
@ -352,14 +377,14 @@ def import_events_from_url(self, url, cat, tags, force=False):
@app.task(base=ChromiumTask, bind=True)
def import_events_from_urls(self, urls_cat_tags):
def import_events_from_urls(self, urls_cat_tags, user_id=None):
for ucat in urls_cat_tags:
if ucat is not None:
url = ucat[0]
cat = ucat[1]
tags = ucat[2]
import_events_from_url.delay(url, cat, tags)
import_events_from_url.delay(url, cat, tags, user_id=user_id)
app.conf.beat_schedule = {
@ -368,6 +393,10 @@ app.conf.beat_schedule = {
# Daily imports at 3:14 a.m.
"schedule": crontab(hour=3, minute=14),
},
"daily_screenshot": {
"task": "agenda_culturel.celery.screenshot",
"schedule": crontab(hour=3, minute=3),
},
"weekly_imports": {
"task": "agenda_culturel.celery.weekly_imports",
# Daily imports on Mondays at 2:22 a.m.

View File

@ -11,6 +11,7 @@ class DBImporterEvents:
def __init__(self, celery_id):
self.celery_id = celery_id
self.error_message = ""
self.user_id = None
self.init_result_properties()
self.today = timezone.now().date().isoformat()
@ -34,9 +35,10 @@ class DBImporterEvents:
def get_nb_removed_events(self):
return self.nb_removed
def import_events(self, json_structure):
def import_events(self, json_structure, user_id=None):
print(json_structure)
self.init_result_properties()
self.user_id = user_id
try:
structure = json.loads(json_structure)
@ -95,7 +97,7 @@ class DBImporterEvents:
def save_imported(self):
self.db_event_objects, self.nb_updated, self.nb_removed = Event.import_events(
self.event_objects, remove_missing_from_source=self.url
self.event_objects, remove_missing_from_source=self.url, user_id=self.user_id
)
def is_valid_event_structure(self, event):

View File

@ -44,7 +44,7 @@ from .models import (
Tag,
Event,
Category,
ContactMessage,
Message,
DuplicatedEvents
)
@ -137,7 +137,11 @@ class EventFilter(django_filters.FilterSet):
if self.get_cleaned_data("position") is None or self.get_cleaned_data("radius") is None:
return parent
d = self.get_cleaned_data("radius")
p = self.get_cleaned_data("position").location
p = self.get_cleaned_data("position")
if not isinstance(d, str) or not isinstance(p, ReferenceLocation):
return parent
p = p.location
return parent.exclude(exact_location=False).filter(exact_location__location__distance_lt=(p, D(km=d)))
def get_url(self):
@ -188,6 +192,7 @@ class EventFilter(django_filters.FilterSet):
def get_cleaned_data(self, name):
try:
return self.form.cleaned_data[name]
except AttributeError:
@ -356,7 +361,7 @@ class EventFilterAdmin(django_filters.FilterSet):
fields = ["status"]
class ContactMessagesFilterAdmin(django_filters.FilterSet):
class MessagesFilterAdmin(django_filters.FilterSet):
closed = django_filters.MultipleChoiceFilter(
label="Status",
choices=((True, _("Closed")), (False, _("Open"))),
@ -369,7 +374,7 @@ class ContactMessagesFilterAdmin(django_filters.FilterSet):
)
class Meta:
model = ContactMessage
model = Message
fields = ["closed", "spam"]

View File

@ -16,6 +16,7 @@ from django.forms import (
)
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
from .utils import PlaceGuesser
from .models import (
Event,
RecurrentImport,
@ -23,7 +24,7 @@ from .models import (
Place,
Category,
Tag,
ContactMessage
Message
)
from django.conf import settings
from django.core.files import File
@ -32,7 +33,6 @@ from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc
from django.utils.safestring import mark_safe
from django.utils.timezone import localtime
from django.utils.formats import localize
from .templatetags.event_extra import event_field_verbose_name, field_to_html
import os
@ -74,7 +74,7 @@ class GroupFormMixin:
return [f for f in self.visible_fields() if not hasattr(f.field, "toggle_group") and (not hasattr(f.field, "group_id") or f.field.group_id == None)]
def fields_by_group(self):
return [(g, self.get_fields_in_group(g)) for g in self.groups] + [(None, self.get_no_group_fields())]
return [(g, self.get_fields_in_group(g)) for g in self.groups] + [(GroupFormMixin.FieldGroup("other", _("Other")), self.get_no_group_fields())]
def clean(self):
result = super().clean()
@ -189,6 +189,7 @@ class EventForm(GroupFormMixin, ModelForm):
old_local_image = CharField(widget=HiddenInput(), required=False)
simple_cloning = CharField(widget=HiddenInput(), required=False)
cloning = CharField(widget=HiddenInput(), required=False)
tags = MultipleChoiceField(
label=_("Tags"),
@ -205,7 +206,11 @@ class EventForm(GroupFormMixin, ModelForm):
"modified_date",
"moderated_date",
"import_sources",
"image"
"image",
"moderated_by_user",
"modified_by_user",
"created_by_user",
"imported_by_user"
]
widgets = {
"start_day": TextInput(
@ -363,6 +368,9 @@ class EventModerateForm(ModelForm):
"exact_location",
"tags"
]
widgets = {
"status": RadioSelect
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -493,7 +501,7 @@ class SelectEventInList(Form):
super().__init__(*args, **kwargs)
self.fields["event"].choices = [
(e.pk, str(e.start_day) + " " + e.title + ((", " + e.location) if e.location else "")) for e in events
(e.pk, (e.start_time.strftime('%H:%M') + " : " if e.start_time else "") + e.title + ((", " + e.location) if e.location else "")) for e in events
]
@ -541,17 +549,17 @@ class MergeDuplicates(Form):
'<li><a href="' + e.get_absolute_url() + '">' + e.title + "</a></li>"
)
result += (
"<li>Création&nbsp;: " + localize(localtime(e.created_date)) + "</li>"
"<li>Création&nbsp;: " + localize(e.created_date) + "</li>"
)
result += (
"<li>Dernière modification&nbsp;: "
+ localize(localtime(e.modified_date))
+ localize(e.modified_date)
+ "</li>"
)
if e.imported_date:
result += (
"<li>Dernière importation&nbsp;: "
+ localize(localtime(e.imported_date))
+ localize(e.imported_date)
+ "</li>"
)
result += "</ul>"
@ -602,7 +610,7 @@ class MergeDuplicates(Form):
result += '<input id="' + id + '" name="' + key + '"'
if key in MergeDuplicates.checkboxes_fields:
result += ' type="checkbox"'
if value in checked:
if checked and value in checked:
result += " checked"
else:
result += ' type="radio"'
@ -634,7 +642,7 @@ class MergeDuplicates(Form):
result = []
for s in selected:
for e in self.duplicates.get_duplicated():
if e.pk == selected:
if e.pk == s:
result.append(e)
break
return result
@ -718,7 +726,7 @@ class EventAddPlaceForm(Form):
return self.instance
class PlaceForm(ModelForm):
class PlaceForm(GroupFormMixin, ModelForm):
required_css_class = 'required'
apply_to_all = BooleanField(
@ -734,24 +742,70 @@ class PlaceForm(ModelForm):
fields = "__all__"
widgets = {"location": TextInput()}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_group('header', _('Header'))
self.fields['name'].group_id = 'header'
self.add_group('address', _('Address'))
self.fields['address'].group_id = 'address'
self.fields['postcode'].group_id = 'address'
self.fields['city'].group_id = 'address'
self.fields['location'].group_id = 'address'
self.add_group('meta', _('Meta'))
self.fields['aliases'].group_id = 'meta'
self.add_group('information', _('Information'))
self.fields['description'].group_id = 'information'
def as_grid(self):
return mark_safe(
'<div class="grid"><div>'
result = ('<div class="grid"><div>'
+ super().as_p()
+ '</div><div><div class="map-widget">'
+ '<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div><p>Cliquez pour ajuster la position GPS</p></div></div></div>'
)
+ '''</div><div><div class="map-widget">
<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div>
<p>Cliquez pour ajuster la position GPS</p></div>
<input type="checkbox" role="switch" id="lock_position">Verrouiller la position</lock>
<script>
document.getElementById("lock_position").onclick = function() {
const field = document.getElementById("id_location");
if (this.checked)
field.setAttribute("readonly", true);
else
field.removeAttribute("readonly");
}
</script>
</div></div>''')
return mark_safe(result)
def apply(self):
return self.cleaned_data.get("apply_to_all")
class ContactMessageForm(ModelForm):
class MessageForm(ModelForm):
class Meta:
model = ContactMessage
model = Message
fields = ["subject", "name", "email", "message", "related_event"]
widgets = {"related_event": HiddenInput()}
widgets = {"related_event": HiddenInput(), "user": HiddenInput() }
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event", False)
self.internal = kwargs.pop("internal", False)
super().__init__(*args, **kwargs)
self.fields['related_event'].required = False
if self.internal:
self.fields.pop("name")
self.fields.pop("email")
class MessageEventForm(ModelForm):
class Meta:
model = Message
fields = ["message"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["message"].label = _("Add a comment")

View File

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

View File

@ -0,0 +1,91 @@
from ..generic_extractors import *
from bs4 import BeautifulSoup
from datetime import datetime
# A class dedicated to get events from Cinéma Le Rio (Clermont-Ferrand)
# URL: https://www.cinemalerio.com/evenements/
class CExtractor(TwoStepsExtractorNoPause):
def __init__(self):
super().__init__()
self.possible_dates = {}
self.theater = None
def build_event_url_list(self, content, infuture_days=180):
soup = BeautifulSoup(content, "html.parser")
links = soup.select("td.seance_link a")
if links:
for l in links:
print(l["href"])
self.add_event_url(l["href"])
def to_text_select_one(soup, filter):
e = soup.select_one(filter)
if e is None:
return None
else:
return e.text
def add_event_from_content(
self,
event_content,
event_url,
url_human=None,
default_values=None,
published=False,
):
soup = BeautifulSoup(event_content, "html.parser")
title = soup.select_one("h1").text
alerte_date = CExtractor.to_text_select_one(soup, ".alerte_date")
if alerte_date is None:
return
dh = alerte_date.split("à")
# if date is not found, we skip
if len(dh) != 2:
return
date = Extractor.parse_french_date(dh[0], default_year=datetime.now().year)
time = Extractor.parse_french_time(dh[1])
synopsis = CExtractor.to_text_select_one(soup, ".synopsis_bloc")
special_titre = CExtractor.to_text_select_one(soup, ".alerte_titre")
special = CExtractor.to_text_select_one(soup, ".alerte_text")
# it's not a specific event: we skip it
special_lines = None if special is None else special.split('\n')
if special is None or len(special_lines) == 0 or \
(len(special_lines) == 1 and special_lines[0].strip().startswith('En partenariat')):
return
description = "\n\n".join([x for x in [synopsis, special_titre, special] if not x is None])
image = soup.select_one(".col1 img")
image_alt = None
if not image is None:
image_alt = image["alt"]
image = image["src"]
self.add_event_with_props(
default_values,
event_url,
title,
None,
date,
None,
description,
[],
recurrences=None,
uuids=[event_url],
url_human=event_url,
start_time=time,
end_day=None,
end_time=None,
published=published,
image=image,
image_alt=image_alt
)

View File

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

View File

@ -49,7 +49,7 @@ class Extractor(ABC):
return i + 1
return None
def parse_french_date(text):
def parse_french_date(text, default_year=None):
# format NomJour Numero Mois Année
m = re.search(
"[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text
@ -73,8 +73,15 @@ class Extractor(ABC):
month = int(m.group(2))
year = m.group(3)
else:
# TODO: consolider les cas non satisfaits
return None
# format Numero Mois Annee
m = re.search("([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)", text)
if m:
day = m.group(1)
month = Extractor.guess_month(m.group(2))
year = default_year
else:
# TODO: consolider les cas non satisfaits
return None
if month is None:
return None
@ -254,9 +261,9 @@ class EventNotFoundExtractor(Extractor):
self.set_header(url)
self.clear_events()
self.add_event(default_values, "événement sans titre",
self.add_event(default_values, "événement sans titre depuis " + url,
None, timezone.now().date(), None,
"l'import a échoué, la saisie doit se faire manuellement à partir de l'url source",
"l'import a échoué, la saisie doit se faire manuellement à partir de l'url source " + url,
[], [url], published=False, url_human=url)
return self.get_structure()

View File

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

View File

@ -264,7 +264,10 @@ class TwoStepsExtractorNoPause(TwoStepsExtractor):
only_future=True,
ignore_404=True
):
pause = self.downloader.pause
if hasattr(self.downloader, "pause"):
pause = self.downloader.pause
else:
pause = False
self.downloader.pause = False
result = super().extract(content, url, url_human, default_values, published, only_future, ignore_404)
self.downloader.pause = pause

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View 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'),
),
]

View File

@ -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'},
),
]

View 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'),
),
]

View File

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

View 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'),
),
]

View File

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

View File

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

View File

@ -10,7 +10,11 @@ from colorfield.fields import ColorField
from django_ckeditor_5.fields import CKEditor5Field
from urllib.parse import urlparse
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.contrib.auth.models import User, AnonymousUser
import emoji
from django.core.files.storage import default_storage
import uuid
import hashlib
import urllib.request
@ -20,6 +24,7 @@ from django.utils import timezone
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q, Count, F, Subquery, OuterRef, Func
from django.db.models.functions import Lower
from django.contrib.postgres.lookups import Unaccent
import recurrence.fields
import recurrence
import copy
@ -284,6 +289,10 @@ class DuplicatedEvents(models.Model):
class Meta:
verbose_name = _("Duplicated events")
verbose_name_plural = _("Duplicated events")
indexes = [
models.Index(fields=['representative']),
]
def __init__(self, *args, **kwargs):
self.events = None
@ -434,8 +443,9 @@ class Place(models.Model):
blank=True,
null=True,
)
postcode = models.CharField(verbose_name=_("Postcode"), help_text=_("The post code is not displayed, but makes it easier to find an address when you enter it."), blank=True, null=True)
city = models.CharField(verbose_name=_("City"), help_text=_("City name"))
location = LocationField(based_fields=["name", "address", "city"], zoom=12, default=Point(3.08333, 45.783329))
location = LocationField(based_fields=["name", "address", "postcode", "city"], zoom=12, default=Point(3.08333, 45.783329))
description = CKEditor5Field(
verbose_name=_("Description"),
@ -458,6 +468,11 @@ class Place(models.Model):
verbose_name = _("Place")
verbose_name_plural = _("Places")
ordering = ["name"]
indexes = [
models.Index(fields=['name']),
models.Index(fields=['city']),
models.Index(fields=['location']),
]
def __str__(self):
if self.address:
@ -543,7 +558,7 @@ class Organisation(models.Model):
return self.name
def get_absolute_url(self):
return reverse("view_organisation", kwargs={'pk': self.pk})
return reverse("view_organisation", kwargs={'pk': self.pk, "extra": self.name})
@ -558,6 +573,39 @@ class Event(models.Model):
modified_date = models.DateTimeField(blank=True, null=True)
moderated_date = models.DateTimeField(blank=True, null=True)
created_by_user = models.ForeignKey(
User,
verbose_name=_("Author of the event creation"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
related_name="created_events"
)
imported_by_user = models.ForeignKey(
User,
verbose_name=_("Author of the last importation"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
related_name="imported_events"
)
modified_by_user = models.ForeignKey(
User,
verbose_name=_("Author of the last modification"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
related_name="modified_events"
)
moderated_by_user = models.ForeignKey(
User,
verbose_name=_("Author of the last moderation"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
related_name="moderated_events"
)
recurrence_dtstart = models.DateTimeField(editable=False, blank=True, null=True)
recurrence_dtend = models.DateTimeField(editable=False, blank=True, null=True)
@ -692,6 +740,10 @@ class Event(models.Model):
blank=True,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.processing_user = None
def get_consolidated_end_day(self, intuitive=True):
if intuitive:
end_day = self.get_consolidated_end_day(False)
@ -709,15 +761,6 @@ class Event(models.Model):
last = self.get_consolidated_end_day()
return [first + timedelta(n) for n in range(int((last - first).days) + 1)]
def get_nb_events_same_dates(self, remove_same_dup=True):
first = self.start_day
last = self.get_consolidated_end_day()
ignore_dup = None
if remove_same_dup:
ignore_dup = self.other_versions
calendar = CalendarList(first, last, exact=True, ignore_dup=ignore_dup)
return [(len(d.events), d.date) for dstr, d in calendar.get_calendar_days().items()]
def is_single_day(self, intuitive=True):
return self.start_day == self.get_consolidated_end_day(intuitive)
@ -727,6 +770,15 @@ class Event(models.Model):
end_date = parse_date(end_date)
return parse_date(self.start_day) + timedelta(days=min_days) < end_date
def set_message(self, msg):
self._message = msg
def get_message(self):
return self._message
def has_message(self):
return hasattr(self, '_message')
def contains_date(self, d, intuitive=True):
return d >= self.start_day and d <= self.get_consolidated_end_day(intuitive)
@ -764,9 +816,36 @@ class Event(models.Model):
permissions = [("set_duplicated_event", "Can set an event as duplicated")]
indexes = [
models.Index(fields=["start_day", "start_time"]),
models.Index("start_time", Lower("title"), name="start_time title")
models.Index(fields=["end_day", "end_time"]),
models.Index(fields=["status"]),
models.Index(fields=["recurrence_dtstart", "recurrence_dtend"]),
models.Index("start_time", Lower("title"), name="start_time title"),
models.Index("start_time", "start_day", "end_day", "end_time", Lower("title"), name="datetimes title")
]
def chronology(self):
c = []
if self.modified_date:
c.append({ "timestamp": self.modified_date, "data": "modified_date", "user": self.modified_by_user, "is_date": True })
if self.moderated_date:
c.append({ "timestamp": self.moderated_date, "data": "moderated_date", "user" : self.moderated_by_user, "is_date": True})
if self.imported_date:
c.append({ "timestamp": self.imported_date, "data": "imported_date", "user": self.imported_by_user, "is_date": True })
if self.created_date:
c.append({ "timestamp": self.created_date + timedelta(milliseconds=-1), "data": "created_date", "user": self.created_by_user, "is_date": True})
c += [{ "timestamp": m.date, "data": m, "user": m.user, "is_date": False} for m in self.message_set.filter(spam=False)]
if self.other_versions:
for o in self.other_versions.get_duplicated():
if o != self:
c += [{ "timestamp": m.date, "data": m, "user": m.user, "is_date": False} for m in o.message_set.filter(spam=False)]
c.sort(key=lambda x: x["timestamp"])
return c
def sorted_tags(self):
if self.tags is None:
return []
@ -858,19 +937,28 @@ class Event(models.Model):
def is_representative(self):
return self.other_versions is None or self.other_versions.representative == self
def download_missing_image(self):
if self.local_image and not default_storage.exists(self.local_image.name):
logger.warning("on dl")
self.download_image()
self.save(update_fields=["local_image"])
def download_image(self):
# first download file
a = urlparse(self.image)
basename = os.path.basename(a.path)
ext = basename.split('.')[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
try:
tmpfile, _ = urllib.request.urlretrieve(self.image)
except:
return None
# if the download is ok, then create the corresponding file object
self.local_image = File(name=basename, file=open(tmpfile, "rb"))
self.local_image = File(name=filename, file=open(tmpfile, "rb"))
def add_pending_organisers(self, organisers):
self.pending_organisers = organisers
@ -896,6 +984,12 @@ class Event(models.Model):
def set_no_modification_date_changed(self):
self.no_modification_date_changed = True
def set_processing_user(self, user):
if user is None or user.is_anonymous:
self.processing_user = None
else:
self.processing_user = user
def set_in_moderation_process(self):
self.in_moderation_process = True
@ -906,12 +1000,16 @@ class Event(models.Model):
now = timezone.now()
if not self.id:
self.created_date = now
self.created_by_user = self.processing_user
if self.is_in_importation_process():
self.imported_date = now
self.imported_by_user = self.processing_user
if self.modified_date is None or not self.is_no_modification_date_changed():
self.modified_date = now
self.modified_by_user = self.processing_user
if self.is_in_moderation_process():
self.moderated_date = now
self.moderated_by_user = self.processing_user
def get_recurrence_at_date(self, year, month, day):
dtstart = timezone.make_aware(
@ -923,10 +1021,13 @@ class Event(models.Model):
else:
return recurrences[0]
def get_image_url(self):
def get_image_url(self, request=None):
if self.local_image and hasattr(self.local_image, "url"):
try:
return self.local_image.url
if request:
return request.build_absolute_uri(self.local_image.url)
else:
return self.local_image.url
except:
pass
if self.image:
@ -1024,7 +1125,7 @@ class Event(models.Model):
self.update_recurrence_dtstartend()
# if the image is defined but not locally downloaded
if self.image and not self.local_image:
if self.image and (not self.local_image or not default_storage.exists(self.local_image.name)):
self.download_image()
# remove "/" from tags
@ -1076,6 +1177,11 @@ class Event(models.Model):
# first save the current object
super().save(*args, **kwargs)
# clear cache
for is_auth in [False, True]:
key = make_template_fragment_key("event_body", [is_auth, self])
cache.delete(key)
# then if its a clone, update the representative
if clone:
self.other_versions.representative = self
@ -1322,9 +1428,10 @@ class Event(models.Model):
# otherwise merge existing groups
group = DuplicatedEvents.merge_groups(groups)
group.save()
if force_non_fixed:
group.representative = None
group.save()
# set the possibly duplicated group for the current object
self.other_versions = group
@ -1345,6 +1452,8 @@ class Event(models.Model):
"category",
"tags",
]
if not no_m2m:
result += ["organisers"]
result += [
"title",
@ -1356,8 +1465,6 @@ class Event(models.Model):
"description",
"image",
]
if not no_m2m:
result += ["organisers"]
if all and local_img:
result += ["local_image"]
if all and exact_location:
@ -1409,7 +1516,11 @@ class Event(models.Model):
self.import_sources.append(source)
# Limitation: the given events should not be considered similar one to another...
def import_events(events, remove_missing_from_source=None):
def import_events(events, remove_missing_from_source=None, user_id=None):
user = None
if user_id:
user = User.objects.filter(pk=user_id).first()
to_import = []
to_update = []
@ -1436,6 +1547,7 @@ class Event(models.Model):
# imported events should be updated
event.set_in_importation_process()
event.set_processing_user(user)
event.prepare_save()
# check if the event has already be imported (using uuid)
@ -1462,9 +1574,14 @@ class Event(models.Model):
same_imported.other_versions.representative = None
same_imported.other_versions.save()
# we only update local information if it's a pure import and has no moderated_date
new_image = same_imported.image != event.image
same_imported.update(event, pure and same_imported.moderated_date is None)
same_imported.set_in_importation_process()
same_imported.prepare_save()
# fix missing or updated files
if same_imported.local_image and (not default_storage.exists(same_imported.local_image.name) or new_image):
same_imported.download_image()
same_imported.save(update_fields=["local_image"])
to_update.append(same_imported)
else:
# otherwise, the new event possibly a duplication of the remaining others.
@ -1492,6 +1609,7 @@ class Event(models.Model):
for e in to_import:
if e.is_event_long_duration():
e.status = Event.STATUS.DRAFT
e.set_message(_("The duration of the event is a little too long for direct publication. Moderators can choose to publish it or not."))
# then import all the new events
imported = Event.objects.bulk_create(to_import)
@ -1499,6 +1617,9 @@ class Event(models.Model):
for i, ti in zip(imported, to_import):
if ti.has_pending_organisers() and ti.pending_organisers is not None:
i.organisers.set(ti.pending_organisers)
if ti.has_message():
msg = Message(subject=_('Import'), related_event=i, name=_('import process'), message=ti.get_message())
msg.save()
nb_updated = Event.objects.bulk_update(
to_update,
@ -1572,13 +1693,12 @@ class Event(models.Model):
def get_concurrent_events(self, remove_same_dup=True):
day = self.current_date if hasattr(self, "current_date") else self.start_day
day_events = CalendarDay(self.start_day).get_events()
day_events = CalendarDay(day, qs = Event.objects.filter(status=Event.STATUS.PUBLISHED)).get_events()
return [
e
for e in day_events
if e != self
and self.is_concurrent_event(e, day)
and e.status == Event.STATUS.PUBLISHED
and (e.other_versions is None or e.other_versions != self.other_versions)
]
@ -1588,10 +1708,10 @@ class Event(models.Model):
return (dtstart <= e_dtstart <= dtend) or (e_dtstart <= dtstart <= e_dtend)
def export_to_ics(events):
def export_to_ics(events, request):
cal = icalCal()
# Some properties are required to be compliant
cal.add("prodid", "-//My calendar product//example.com//")
cal.add("prodid", "-//Pommes de lune//pommesdelune.fr//")
cal.add("version", "2.0")
for event in events:
@ -1642,9 +1762,12 @@ class Event(models.Model):
eventIcal.add("summary", event.title)
eventIcal.add("name", event.title)
url = ("\n" + event.reference_urls[0]) if event.reference_urls and len(event.reference_urls) > 0 else ""
description = event.description if event.description else ""
eventIcal.add(
"description", event.description + url
"description", description + url
)
if not event.local_image is None and event.local_image != "":
eventIcal.add('image', request.build_absolute_uri(event.local_image), parameters={'VALUE': 'URI'})
eventIcal.add("location", event.exact_location or event.location)
cal.add_component(eventIcal)
@ -1680,10 +1803,18 @@ class Event(models.Model):
return [Event.get_count_modification(w) for w in when_list]
class ContactMessage(models.Model):
class Message(models.Model):
class Meta:
verbose_name = _("Contact message")
verbose_name_plural = _("Contact messages")
verbose_name = _("Message")
verbose_name_plural = _("Messages")
indexes = [
models.Index(fields=['related_event']),
models.Index(fields=['user']),
models.Index(fields=['date']),
models.Index(fields=['spam', 'closed']),
]
subject = models.CharField(
verbose_name=_("Subject"),
@ -1700,6 +1831,14 @@ class ContactMessage(models.Model):
on_delete=models.SET_DEFAULT,
)
user = models.ForeignKey(
User,
verbose_name=_("Author of the message"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
)
name = models.CharField(
verbose_name=_("Name"),
help_text=_("Your name"),
@ -1739,11 +1878,11 @@ class ContactMessage(models.Model):
null=True,
)
def nb_open_contactmessages():
return ContactMessage.objects.filter(closed=False).count()
def nb_open_messages():
return Message.objects.filter(Q(closed=False)&Q(spam=False)).count()
def get_absolute_url(self):
return reverse("contactmessage", kwargs={"pk": self.pk})
return reverse("message", kwargs={"pk": self.pk})
class RecurrentImport(models.Model):
@ -1764,6 +1903,7 @@ class RecurrentImport(models.Model):
FBEVENTS = "Facebook events", _("Événements d'une page FB")
C3C = "cour3coquins", _("la cour des 3 coquins")
ARACHNEE = "arachnee", _("Arachnée concert")
LERIO = "rio", _('Le Rio')
class DOWNLOADER(models.TextChoices):
SIMPLE = "simple", _("simple")
@ -1831,6 +1971,12 @@ class RecurrentImport(models.Model):
blank=True,
)
forceLocation = models.BooleanField(
verbose_name=_("Force location"),
help_text=_("force location even if another is detected."),
default=False
)
defaultOrganiser = models.ForeignKey(
Organisation,
verbose_name=_("Organiser"),
@ -1891,6 +2037,11 @@ class BatchImportation(models.Model):
verbose_name = _("Batch importation")
verbose_name_plural = _("Batch importations")
permissions = [("run_batchimportation", "Can run a batch importation")]
indexes = [
models.Index(fields=['created_date']),
models.Index(fields=['status']),
models.Index(fields=['created_date', 'recurrentImport']),
]
created_date = models.DateTimeField(auto_now_add=True)

View File

@ -56,9 +56,10 @@ INSTALLED_APPS = [
"robots",
"debug_toolbar",
"cache_cleaner",
"honeypot",
]
SITE_ID = 1
HONEYPOT_FIELD_NAME = "alias_name"
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
@ -72,6 +73,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
'django.contrib.sites.middleware.CurrentSiteMiddleware',
# "django.middleware.cache.UpdateCacheMiddleware",
# "django.middleware.common.CommonMiddleware",
# "django.middleware.cache.FetchFromCacheMiddleware",
@ -145,7 +147,7 @@ TIME_ZONE = "Europe/Paris"
USE_I18N = True
USE_TZ = True
USE_TZ = False
LANGUAGES = (
("fr", _("French")),

View 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

View File

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

View 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* Leaflets 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 dont 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 );
}
}

View File

@ -44,6 +44,9 @@ $enable-responsive-typography: true;
// Modal (<dialog>)
--modal-overlay-backdrop-filter: blur(0.05rem);
--background-color-transparent: color-mix(in srgb, var(--background-color), transparent 30%);
--background-color-transparent-light: color-mix(in srgb, var(--background-color), transparent 80%);
}
@ -329,6 +332,7 @@ footer [data-tooltip] {
scroll-behavior: smooth;
transition-duration: 200ms;
.cat {
margin-right: 0;
}
@ -1434,6 +1438,17 @@ img.preview {
}
}
.header-complement {
float: none;
}
@media only screen and (min-width: 992px) {
.header-complement {
float: left;
clear: both;
}
}
form.messages div, form.moderation-events {
@media only screen and (min-width: 992px) {
display: grid;
@ -1454,6 +1469,11 @@ form.messages div, form.moderation-events {
overflow-y: auto;
}
#moderate-form #id_status {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
label.required::after {
content: ' *';
color: red;
@ -1502,3 +1522,228 @@ label.required::after {
.maskable_group .body_group.closed {
display: none;
}
.form-place {
display: grid;
grid-template-columns: repeat(1, 1fr);
row-gap: .5em;
margin-bottom: 0.5em;
.map-widget {
grid-row: 3;
}
#group_address .body_group {
display: grid;
grid-template-columns: repear(2, 1fr);
column-gap: .5em;
#div_id_address, #div_id_location {
grid-column: 1 / 3;
}
}
}
@media only screen and (min-width: 992px) {
.form-place {
grid-template-columns: repeat(2, 1fr);
.map-widget {
grid-column: 2 / 3;
grid-row: 1 / 3;
}
#group_other {
grid-column: 1 / 3;
}
}
}
.line-now {
font-size: 60%;
div {
display: grid;
grid-template-columns: fit-content(2em) auto;
column-gap: .2em;
color: red;
.line {
margin-top: .7em;
border-top: 1px solid red;
}
}
margin-bottom: 0;
list-style: none;
}
.a-venir .line-now {
margin-left: -2em;
}
#chronology {
.entree {
display: grid;
grid-template-columns: fit-content(2em) auto;
column-gap: .7em;
.texte {
background: var(--background-color);
padding: 0.1em 0.8em;
border-radius: var(--border-radius);
p {
font-size: 100%;
}
p:last-child {
margin-bottom: 0.1em;
}
}
}
font-size: 85%;
footer {
margin-top: 1.8em;
padding: 0.2em .8em;
}
.ts {
@extend .badge-small;
border-radius: var(--border-radius);
display: inline-block;
width: 14em;
margin-right: 1.2em;
}
}
.moderation_heatmap {
overflow-x: auto;
table {
max-width: 600px;
margin: auto;
.total, .month {
display: none;
}
.label {
display: none;
}
th {
font-size: 90%;
text-align: center;
}
td {
font-size: 80%;
text-align: center;
}
tbody th {
text-align: right;
}
.ratio {
padding: 0.1em;
a, .a {
margin: auto;
border-radius: var(--border-radius);
color: black;
padding: 0;
display: block;
max-width: 6em;
width: 3.2em;
height: 2em;
line-height: 2em;
text-decoration: none;
}
}
.score_0 {
a, .a {
background: rgb(0, 128, 0);
}
a:hover {
background: rgb(0, 176, 0);
}
}
.score_1 {
a, .a {
background: rgb(255, 255, 0);
}
a:hover {
background: rgb(248, 248, 121);
}
}
.score_2 {
a, .a {
background: rgb(255, 166, 0);
}
a:hover {
background: rgb(255, 182, 47);
}
}
.score_3 {
a, .a {
background: rgb(255, 0, 0);
}
a:hover {
background: rgb(255, 91, 91);
}
}
.score_4 {
a, .a {
background: rgb(128, 0, 128);
color: white;
}
a:hover {
background: rgb(178, 0, 178);
}
}
@media only screen and (min-width: 1800px) {
.total, .month {
display: inline;
opacity: .35;
}
.label {
display: table-cell;
}
}
@media only screen and (min-width: 1600px) {
.ratio {
a, .a {
width: 5em;
height: 3.2em;
line-height: 3.2em;
}
}
font-size: 100%;
}
}
}
dialog {
.h-image {
background-repeat: no-repeat;
background-size: cover;
background-position: center, center;
}
.h-mask {
background-color: var(--background-color);
margin: calc(var(--spacing) * -1.5);
padding: calc(var(--spacing) * 1.5);
}
.h-mask.visible {
background-color: var(--background-color-transparent);
transition: background-color .8s ease-in;
}
.h-mask.visible:hover {
background-color: var(--background-color-transparent-light);
}
}
.visible-link {
text-decoration: underline;
}
.detail-link {
text-align: right;
padding-right: 0.4em;
.visible-link {
color: var(--contrast);
}
}
.week-in-month {
article {
.visible-link {
color: var(--contrast);
}
}
}

View File

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

View File

@ -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&nbsp;? 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 %}

View File

@ -12,7 +12,7 @@
{% if local %}
<a href="{{ local.get_absolute_url }}" role="button">voir la version locale {% picto_from_name "eye" %}</a>
{% else %}
<a href="{% url 'clone_edit' event.id %}" role="button">créer une copie locale {% picto_from_name "plus-circle" %}</a>
<a href="{% url 'clone_edit' event.id %}" role="button">modifier en copie locale {% picto_from_name "plus-circle" %}</a>
{% endif %}
{% endwith %}
{% else %}

View File

@ -1,13 +1,13 @@
{% if user.is_authenticated %}
<p class="footer">Création&nbsp;: {{ event.created_date }}
<p class="footer">Création&nbsp;: {{ event.created_date }}{% if event.created_by_user %} par <em>{{ event.created_by_user.username }}</em>{% endif %}
{% if event.modified %}
— dernière modification&nbsp;: {{ event.modified_date }}
— dernière modification&nbsp;: {{ 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&nbsp;: {{ event.imported_date }}
— dernière importation&nbsp;: {{ 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&nbsp;: {{ event.moderated_date }}
— dernière modération&nbsp;: {{ 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>

View File

@ -1,10 +1,12 @@
<footer class="remarque">
Informations complémentaires non éditables&nbsp;:
<strong>Informations complémentaires non éditables</strong>
<ul>
{% if object.created_date %}<li>Création&nbsp;: {{ object.created_date }}</li>{% endif %}
{% if object.modified_date %}<li>Dernière modification&nbsp;: {{ object.modified_date }}</li>{% endif %}
{% if object.moderated_date %}<li>Dernière modération&nbsp;: {{ object.moderated_date }}</li>{% endif %}
{% if object.imported_date %}<li>Dernière importation&nbsp;: {{ object.imported_date }}</li>{% endif %}
{% if not allbutdates %}
{% if object.created_date %}<li>Création&nbsp;: {{ 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&nbsp;: {{ 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&nbsp;: {{ 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&nbsp;: {{ 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)&nbsp;:

View File

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

View File

@ -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">&lt; 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 &gt;" name="save_and_next">
<a href="{% url 'moderate_event_next' event.pk %}" class="secondary" role="button">Passer au suivant sans enregistrer &gt;</a>
</div>
</form>
</article>

View File

@ -1,5 +1,6 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% load honeypot %}
{% block title %}{% block og_title %}{% if form.event %}Contact au sujet de l'événement {{ form.event.title }}{% else %}
Contact{% endif %}{% endblock %}{% endblock %}
@ -31,7 +32,7 @@ Contact{% endif %}{% endblock %}{% endblock %}
</article>
{% endif %}
<p class="message warning"><strong>Attention&nbsp:</strong> n'utilisez pas le formulaire ci-dessous pour proposer un événement, il sera ignoré. Utilisez plutôt la page <a href="{% url 'add_event' %}">ajouter un événement</a>.</p>
<p class="message warning"><strong>Attention&nbsp;:</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 «&nbsp;{{ form.event.title }}&nbsp;» du {{ form.event.start_day }}.
N'hésites pas à nous indiquer le maximum de contexte et à nous laisser ton adresse
@ -40,6 +41,7 @@ Contact{% endif %}{% endblock %}{% endblock %}
{% endif %}
</header>
<form method="post">{% csrf_token %}
{% render_honeypot_field "alias_name" %}
{{ form.media }}
{{ form.as_p }}
<input type="submit" value="Envoyer">

View File

@ -24,13 +24,13 @@
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'delete_contactmessage' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
<a href="{% url 'delete_message' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
<h1>Modération du message «&nbsp;{{ object.subject }}&nbsp;»</h1>
<ul>
<li>Date&nbsp;: {{ object.date.date }} à {{ object.date.time }}</li>
<li>Auteur&nbsp;: {{ object.name }} {% if object.email %}<a href="mailto:{{ object.email }}">{{ object.email }}</a>{% endif %}</li>
<li>Auteur&nbsp;: {% 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é&nbsp;: <a href="{{ object.related_event.get_absolute_url }}">{{ object.related_event.title }}</a> du {{ object.related_event.start_day }}</li>{% endif %}
</ul>
</header>
@ -47,7 +47,7 @@
</article>
</div>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
{% include "agenda_culturel/side-nav.html" with current="messages" %}
</div>
{% endblock %}

View File

@ -45,8 +45,8 @@
{% for obj in paginator_filter %}
<tr>
<td>{{ obj.date }}</td>
<td><a href="{% url 'contactmessage' obj.pk %}">{{ obj.subject }}</a></td>
<td>{{ obj.name }}</td>
<td><a href="{% url 'message' obj.pk %}">{{ obj.subject }}</a></td>
<td>{% if obj.user %}<em>{{ obj.user }}</em>{% else %}{{ obj.name }}{% endif %}</td>
<td>{% if obj.related_event %}<a href="{{ obj.related_event.get_absolute_url }}">{{ obj.related_event.pk }}</a>{% else %}/{% endif %}</td>
<td>{% if obj.closed %}{% picto_from_name "check-square" "fermé" %}{% else %}{% picto_from_name "square" "ouvert" %}{% endif %}</td>
<td>{% if obj.spam %}{% picto_from_name "check-square" "spam" %}{% else %}{% picto_from_name "square" "non spam" %}{% endif %}</td>
@ -59,7 +59,7 @@
</footer>
</article>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
{% include "agenda_culturel/side-nav.html" with current="messages" %}
</div>
{% endblock %}

View File

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

View File

@ -3,10 +3,11 @@
{% load cat_extra %}
{% load utils_extra %}
{% load event_extra %}
{% load cache %}
{% block title %}{% block og_title %}{{ event.title }}{% endblock %}{% endblock %}
{% block og_image %}{% if event.has_image_url %}{{ event.get_image_url }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block og_image %}{% if event.has_image_url %}{{ event|get_image_uri:request }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block og_description %}{% if event.description %}{{ event.description |truncatewords:20|linebreaks }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block entete_header %}
@ -16,21 +17,59 @@
{% block content %}
<div class="grid two-columns">
<div>
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout event_body user.is_authenticated event %}
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event filter=filter %}
{% endcache %}
{% endwith %}
{% if user.is_authenticated %}
<article>
<article id="chronology">
<header>
<h2>Informations internes</h2>
<h2>Chronologie</h2>
</header>
{% include "agenda_culturel/event-info-inc.html" with object=event %}
{% for step in event.chronology %}
{% if step.is_date %}
<div class="entree dateline">
<div><span class="ts">{{ step.timestamp }}</span></div>
<div>
{% if step.data == "created_date" %}<em>création</em>{% if event.created_by_user %} par {{ event.created_by_user.username }}{% endif %}{% endif %}
{% if step.data == "modified_date" %}<em>dernière modification</em>{% if event.modified_by_user %} par {{ event.modified_by_user.username }}{% endif %}{% endif %}
{% if step.data == "moderated_date" %}<em>dernière modération</em>{% if event.moderated_by_user %} par {{ event.moderated_by_user.username }}{% endif %}{% endif %}
{% if step.data == "imported_date" %}<em>dernière importation</em>{% if event.imported_by_user %} par {{ event.imported_by_user.username }}{% endif %}{% endif %}
</div>
</div>
{% else %}
<div class="entree">
<div><span class="ts">{{ step.timestamp }}</span></div>
<div>
<header><strong>Message</strong>{% if step.data.related_event and event != step.data.related_event %} sur <a href="{{ step.data.related_event.get_absolute_url }}">une autre</a> version{% endif %}&nbsp;: <a href="{{ step.data.get_absolute_url }}">{{ step.data.subject|truncatechars:20 }}</a> {% if step.data.user %} par <em>{{ step.data.user }}</em>{% else %} par {{ step.data.name }}{% if step.data.email %} (<a href="mailto: {{ step.data.email }}">{{ step.data.email }}</a>){% endif %}{% endif %}</header>
<div class="texte">{{ step.data.message|safe }}</div>
{% if step.data.comments %}
<div><strong>Commentaire&nbsp;:</strong> {{ step.data.comments }}</div>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
<form method="post">{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
<input type="submit" value="Commenter">
</form>
{% include "agenda_culturel/event-info-inc.html" with allbutdates=1 %}
</article>
{% endif %}
</div>
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout event_aside user.is_authenticated event %}
<aside>
{% with event.get_concurrent_events as concurrent_events %}
{% if concurrent_events %}
@ -51,39 +90,7 @@
</article>
{% endif %}
{% endwith %}
<article>
{% with event.get_nb_events_same_dates as nb_events_same_dates %}
{% with nb_events_same_dates|length as c_dates %}
<header>
<h2>Voir aussi</h2>
{% if c_dates != 1 %}
<p class="remarque">
Retrouvez ci-dessous tous les événements
{% if event.is_single_day %}
à la même date
{% else %}
aux mêmes dates
{% endif %}
que l'événement affiché.
</p>
{% endif %}
</header>
<nav>
{% if c_dates == 1 %}
<a role="button" href="{% url 'day_view' nb_events_same_dates.0.1.year nb_events_same_dates.0.1.month nb_events_same_dates.0.1.day %}">Toute la journée</a>
{% else %}
<ul>
{% for nbevents_date in nb_events_same_dates %}
<li>
<a href="{% url 'day_view' nbevents_date.1.year nbevents_date.1.month nbevents_date.1.day %}">{{ nbevents_date.0 }} événement{{ nbevents_date.0 | pluralize }} le {{ nbevents_date.1 }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</nav>
{% endwith %}
{% endwith %}
</article>
{% if event.other_versions and not event.other_versions.fixed %}
{% with poss_dup=event.get_other_versions|only_allowed:user.is_authenticated %}
{% if poss_dup|length > 0 %}
@ -127,8 +134,10 @@
</aside>
{% endcache %}
{% endwith %}
</div>
{% endblock %}

View File

@ -84,7 +84,7 @@
</script>
{% endif %}
<header{% if day.is_today %} id="today"{% endif %}>
<h3><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h3}>
<h3><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">{{ day.date | date:"l j" }}</a></h3}>
</header>
{% if day.events %}
<ul>
@ -121,7 +121,8 @@
</article>
</dialog>
{% endfor %}
<ul>
<li class="detail-link"><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">voir en détail {% picto_from_name "chevrons-right" %}</a></li>
</ul>
{% endif %}
</ul>
</article>

View File

@ -40,7 +40,7 @@
<li><strong>Valeurs par défaut&nbsp;:</strong>
<ul>
<li><strong>Publié&nbsp;:</strong> {{ object.defaultPublished|yesno:"Oui,Non" }}</li>
{% if object.defaultLocation %}<li><strong>Localisation&nbsp;:</strong> {{ object.defaultLocation }}</li>{% endif %}
{% if object.defaultLocation %}<li><strong>Localisation{% if object.forceLocation %} (forcée){% endif %}&nbsp;:</strong> {{ object.defaultLocation }}</li>{% endif %}
<li><strong>Catégorie&nbsp;:</strong> {{ object.defaultCategory }}</li>
{% if object.defaultOrganiser %}<li><strong>Organisateur&nbsp;:</strong> <a href="{{ object.defaultOrganiser.get_absolute_url }}">{{ object.defaultOrganiser }}</a></li>{% endif %}
<li><strong>Étiquettes&nbsp;:</strong>

View File

@ -95,6 +95,9 @@
<h3>{{ ti.short_name }} <a class="badge simple" href="#{{ ti.id }}" data-tooltip="Aller à {{ ti.name }}">{{ ti.events|length }} {% picto_from_name "chevrons-down" %}</a></h3>
<ul>
{% for event in ti.events %}
{% if event.is_first_after_now %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
<li>{{ event.category | circle_cat:event.has_recurrences }}
{% if event.start_time %}
{{ event.start_time }}
@ -102,6 +105,9 @@
{{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title|no_emoji }}</a> {{ event|tw_badge }}
</li>
{% endfor %}
{% if forloop.last and cd.is_today_after_events %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
</ul>
{% endif %}
{% endfor %}

View File

@ -37,7 +37,7 @@
<div>
{% if calendar.firstdate|shift_day:-1|not_before_first %}
{% if calendar.lastdate|not_after_last %}
<a role="button" href="{% url 'week_view' calendar.previous_week.year calendar.previous_week|week %}?{{ filter.get_url }}">
<a role="button" href="{% url 'week_view' calendar.previous_week|weekyear calendar.previous_week|week %}?{{ filter.get_url }}">
{% picto_from_name "chevron-left" %} précédente</a>
{% endif %}
{% endif %}
@ -45,7 +45,7 @@
{% if calendar.lastdate|shift_day:+1|not_after_last %}
{% if calendar.lastdate|not_before_first %}
<div class="right">
<a role="button" href="{% url 'week_view' calendar.next_week.year calendar.next_week|week %}?{{ filter.get_url }}">suivante
<a role="button" href="{% url 'week_view' calendar.next_week|weekyear calendar.next_week|week %}?{{ filter.get_url }}">suivante
{% picto_from_name "chevron-right" %}
</a>
</div>
@ -57,7 +57,7 @@
<div class="slider-button slider-button-inside button-left hidden">{% picto_from_name "arrow-left" %}</div>
{% if calendar.firstdate|shift_day:-1|not_before_first %}
{% if calendar.lastdate|not_after_last %}
<div class="slider-button slider-button-page button-left hidden"><a href="{% url 'week_view' calendar.previous_week.year calendar.previous_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-left" %}</a></div>
<div class="slider-button slider-button-page button-left hidden"><a href="{% url 'week_view' calendar.previous_week|weekyear calendar.previous_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-left" %}</a></div>
{% endif %}
{% endif %}
@ -80,11 +80,14 @@
</script>
{% endif %}
<header{% if day.is_today %} id="today"{% endif %}>
<h2><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h2>
<h2><a class="visible-link" href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h2>
</header>
{% if day.events %}
<ul>
{% for event in day.events %}
{% if event.is_first_after_now %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
<li>{{ event.category | circle_cat:event.has_recurrences }}
{% if event.start_day == day.date and event.start_time %}
{{ event.start_time }}
@ -93,7 +96,12 @@
{{ event|tw_badge }}
<dialog id="event-{{ event.id }}">
<article>
<header>
{% if event.has_image_url %}
<header style="background-image: url({{ event.get_image_url }});" class="h-image">
{% else %}
<header class="cat-{{ event.category.pk }}">
{% endif %}
<div class="h-mask">
<a href="#event-{{ event.id }}"
aria-label="Fermer"
class="close"
@ -125,6 +133,7 @@
{% endif %}
{% if event.end_time %} {% if not event.end_day|date|frdate or event.end_day == event.start_day %}jusqu'à{% endif %} {{ event.end_time }}{% endif %}
</p>
</div>
</header>
<div class="body-fixed">{{ event.description |linebreaks }}</div>
@ -147,6 +156,10 @@
</dialog>
</li>
{% endfor %}
{% if day.is_today_after_events %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
<li class="detail-link"><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">voir en détail {% picto_from_name "chevrons-right" %}</a></li>
</ul>
{% endif %}
</ul>
@ -160,7 +173,7 @@
<div class="slider-button slider-button-inside button-right hidden">{% picto_from_name "arrow-right" %}</div>
{% if calendar.lastdate|shift_day:+1|not_after_last %}
{% if calendar.lastdate|not_before_first %}
<div class="slider-button slider-button-page button-right hidden"><a href="{% url 'week_view' calendar.next_week.year calendar.next_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-right" %}</a></div>
<div class="slider-button slider-button-page button-right hidden"><a href="{% url 'week_view' calendar.next_week|weekyear calendar.next_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-right" %}</a></div>
{% endif %}
{% endif %}

View File

@ -1,5 +1,11 @@
<!DOCTYPE html>
<html lang="fr">
{% load event_extra %}
{% load cache %}
{% load messages_extra %}
{% load utils_extra %}
{% load duplicated_extra %}
{% load rimports_extra %}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -9,7 +15,7 @@
{% load static %}
<meta property="og:title" content="Pommes de lune — {% block og_title %}{% endblock %}" />
<meta property="og:description" content="{% block og_description %}Événements culturels à Clermont-Ferrand et aux environs{% endblock %}" />
<meta property="og:image" content="{% block og_image %}{% static 'images/capture.png' %}{% endblock %}" />
<meta property="og:image" content="{% block og_image %}https://{{ request.get_host }}{% get_media_prefix %}screenshot.png{% endblock %}" />
<meta property="og:url" content="{{ request.build_absolute_uri }}" />
{% if debug %}
@ -27,12 +33,7 @@
{% block entete_header %}
{% endblock %}
</head>
{% load event_extra %}
{% load cache %}
{% load contactmessages_extra %}
{% load utils_extra %}
{% load duplicated_extra %}
{% load rimports_extra %}
<body class="{% block body-class %}contenu{% endblock %}">
<div id="boutons-fixes">
<ul>
@ -50,7 +51,7 @@
{% if user.is_authenticated %}{% block configurer-menu %}<li id="menu-configurer" class="configurer-bouton"><a href="{% url 'administration' %}">Administrer {% picto_from_name "settings" %}</a></li>{% endblock %}{% endif %}
{% block ajouter-menu %}<li id="menu-ajouter" class="ajouter-bouton"><a href="{% url 'add_event' %}">Ajouter un événement {% picto_from_name "plus-circle" %}</a></li>{% endblock %}
{% block rechercher-menu %}<li id="menu-rechercher" class="rechercher-bouton"><a href="{% url 'event_search' %}">Rechercher {% picto_from_name "search" %}</a></li>{% endblock %}
<li><a href="{% url 'a_venir' %}{% block a_venir_parameters %}{% endblock %}">À venir</a></li>
<li><a href="{% url 'a_venir' %}{% block a_venir_parameters %}{% endblock %}">Maintenant</a></li>
<li><a href="{% url 'cette_semaine' %}{% block cette_semaine_parameters %}{% endblock %}">Cette semaine</a></li>
<li><a href="{% url 'ce_mois_ci' %}{% block ce_mois_ci_parameters %}{% endblock %}">Ce mois-ci</a></li>
</ul>
@ -76,8 +77,8 @@
{% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %}
{% show_badge_unknown_places "bottom" %}
{% endif %}
{% if perms.agenda_culturel.view_contactmessage %}
{% show_badge_contactmessages "bottom" %}
{% if perms.agenda_culturel.view_message %}
{% show_badge_messages "bottom" %}
{% endif %}
{% if user.is_authenticated %}
{{ user.username }} @

View File

@ -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&nbsp;:</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">

View File

@ -1,5 +1,5 @@
{% load event_extra %}
{% load contactmessages_extra %}
{% load messages_extra %}
{% load duplicated_extra %}
{% load utils_extra %}
<aside id="sidebar">
@ -56,11 +56,11 @@
</ul>
</nav>
{% endif %}
{% if perms.agenda_culturel.view_contactmessage %}
{% if perms.agenda_culturel.view_message %}
<h3>Messages</h3>
<nav>
<ul>
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
<li><a {% if current == "messages" %}class="selected" {% endif %}href="{% url 'messages' %}">Messages de contact</a>{% show_badge_messages "left" %}</li>
</ul>
</nav>
{% endif %}
@ -68,6 +68,7 @@
<h3>Configuration interne</h3>
<nav>
<ul>
<li><a href="{% url 'clear_cache' %}">Vider le cache</a></li>
<li><a href="{% url 'admin:index' %}">Administration de django</a></li>
</ul>
</nav>

View File

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

View File

@ -22,16 +22,17 @@
{% endif %}
{% if event.end_time %} {% if not event.end_day|date|frdate or event.end_day == event.start_day %}jusqu'à{% endif %} {{ event.end_time }}{% endif %}
</p>
<p>
{% picto_from_name "map-pin" %}
{% if event.exact_location %}
<a href="{{ event.exact_location.get_absolute_url }}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a>
<p>{% picto_from_name "map-pin" %}
<a href="{{ event.exact_location.get_absolute_url }}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a></p>
{% else %}
{% if perms.agenda_culturel.change_event and perms.agenda_culturel.change_place %}
<a href="{% url 'add_place_to_event' event.pk %}" class="missing-data">{{ event.location }}</a>
<p>{% picto_from_name "map-pin" %}
<a href="{% url 'add_place_to_event' event.pk %}" class="missing-data">{% if event.location %}{{ event.location }}{% else %}sans lieu{% endif %}</a></p>
{% else %}
{{ event.location }}
{% if event.location %}<p>{% picto_from_name "map-pin" %} {{ event.location }}</p>{% endif %}
{% endif %}
{% endif %}
</p>
@ -75,10 +76,10 @@
</p>
{% endif %}
{% endif %}
{% if perms.agenda_culturel.change_contactmessage %}
{% if event.contactmessage_set.all.count > 0 %}
<p class="remarque">Cet événement a fait l'objet {% if event.contactmessage_set.all.count == 1 %}d'un signalement{% else %}de signalements{% endif %}
{% for cm in event.contactmessage_set.all %}
{% if perms.agenda_culturel.change_message %}
{% if event.message_set.all.count > 0 %}
<p class="remarque">Cet événement a fait l'objet {% if event.message_set.all.count == 1 %}d'un signalement{% else %}de signalements{% endif %}
{% for cm in event.message_set.all %}
<a href="{{ cm.get_absolute_url }}">le {{ cm.date.date }} à {{ cm.date.time }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
@ -133,9 +134,24 @@
{% include "agenda_culturel/event-date-info-inc.html" %}
</div>
<div class="buttons">
<a href="{% url 'export_event_ical' event.start_day.year event.start_day.month event.start_day.day event.id %}" role="button">Exporter ical {% picto_from_name "calendar" %}</a>
{% if perms.agenda_culturel.change_event and not noedit %}
{% include "agenda_culturel/edit-buttons-inc.html" with event=event with_clone=1 %}
{% if onlyedit %}
{% if event.pure_import %}
{% with event.get_local_version as local %}
{% if local %}
<a href="{{ local.get_absolute_url }}" role="button">voir la version locale {% picto_from_name "eye" %}</a>
{% else %}
<a href="{% url 'clone_edit' event.id %}" role="button">créer une copie locale {% picto_from_name "plus-circle" %}</a>
{% endif %}
{% endwith %}
{% else %}
<a href="{% url 'edit_event' event.id %}" role="button">modifier {% picto_from_name "edit-3" %}</a>
{% endif %}
{% else %}
<a href="{% url 'export_event_ical' event.start_day.year event.start_day.month event.start_day.day event.id %}" role="button">Exporter ical {% picto_from_name "calendar" %}</a>
{% if perms.agenda_culturel.change_event and not noedit %}
{% include "agenda_culturel/edit-buttons-inc.html" with event=event with_clone=1 %}
{% endif %}
{% endif %}
</div>
</footer>

View File

@ -86,12 +86,6 @@ def css_categories():
)
result += "}"
result += "*:hover>." + c["css_class"] + " {"
result += background_color_adjust_color(
adjust_lightness_saturation(c["color"], 0.02, 1.0)
)
result += "}"
result += "." + c["css_class"] + ".circ-cat, "
result += "form ." + c["css_class"] + ", "
result += ".selected ." + c["css_class"] + " {"

View File

@ -179,4 +179,8 @@ def tw_badge(event):
if event.tags and len([t for t in event.tags if t.startswith("TW:")]) > 0:
return mark_safe('<span class="badge tw-badge">TW</span>')
else:
return ""
return ""
@register.filter
def get_image_uri(event, request):
return event.get_image_url(request)

View File

@ -4,7 +4,7 @@ from django.urls import reverse_lazy
from django.template.defaultfilters import pluralize
from agenda_culturel.models import ContactMessage
from agenda_culturel.models import Message
from .utils_extra import picto_from_name
@ -12,15 +12,15 @@ register = template.Library()
@register.simple_tag
def show_badge_contactmessages(placement="top"):
nb_open = ContactMessage.nb_open_contactmessages()
def show_badge_messages(placement="top"):
nb_open = Message.nb_open_messages()
if nb_open != 0:
return mark_safe(
'<a href="'
+ reverse_lazy("contactmessages")
+ reverse_lazy("messages")
+ '?closed=False" class="badge" data-placement="'
+ placement
+ '"data-tooltip="'
+ '" data-tooltip="'
+ str(nb_open)
+ " message"
+ pluralize(nb_open)

View File

@ -29,6 +29,9 @@ def add_de(txt):
def week(d):
return d.isocalendar()[1]
@register.filter
def weekyear(d):
return d.isocalendar()[0]
@register.filter
def not_before_first(d):

View File

@ -4,10 +4,32 @@ from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path, include, re_path
from django.views.i18n import JavaScriptCatalog
from django.contrib.sitemaps.views import sitemap
from django.contrib.sitemaps import GenericSitemap
from .sitemaps import StaticViewSitemap
from django.views.decorators.cache import cache_page
from .views import *
event_dict = {
"queryset": Event.objects.all(),
"date_field": "modified_date",
}
place_dict = {
"queryset": Place.objects.all(),
}
organisation_dict = {
"queryset": Organisation.objects.all(),
}
sitemaps = {
"static": StaticViewSitemap,
"events": GenericSitemap(event_dict, priority=0.7),
"places": GenericSitemap(place_dict, priority=0.6),
"organisations": GenericSitemap(organisation_dict, priority=0.2),
}
urlpatterns = [
path("", home, name="home"),
path("semaine/<int:year>/<int:week>/", week_view, name="week_view"),
@ -35,12 +57,13 @@ urlpatterns = [
),
path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
path("event/<int:pk>/moderate", EventModerateView.as_view(), name="moderate_event"),
path("event/<int:pk>/moderate-next", EventModerateView.as_view(), name="moderate_event_next"),
path("event/<int:pk>/moderate-next/error", error_next_event, name="error_next_event"),
path("event/<int:pk>/moderate/after/<int:pred>", EventModerateView.as_view(), name="moderate_event_step"),
path("event/<int:pk>/moderate-next", moderate_event_next, name="moderate_event_next"),
path("moderate", EventModerateView.as_view(), name="moderate"),
path("moderate/<int:y>/<int:m>/<int:d>", moderate_from_date, name="moderate_from_date"),
path("event/<int:pk>/simple-clone/edit", EventUpdateView.as_view(), name="simple_clone_edit"),
path("event/<int:pk>/clone/edit", EventUpdateView.as_view(), name="clone_edit"),
path("event/<int:pk>/message", ContactMessageCreateView.as_view(), name="message_for_event"),
path("event/<int:pk>/message", MessageCreateView.as_view(), name="message_for_event"),
path("event/<int:pk>/update-from-source", update_from_source, name="update_from_source"),
path(
"event/<int:pk>/change-status/<status>",
@ -75,18 +98,18 @@ urlpatterns = [
path("mentions-legales", mentions_legales, name="mentions_legales"),
path("a-propos", about, name="about"),
path("merci", thank_you, name="thank_you"),
path("contact", ContactMessageCreateView.as_view(), name="contact"),
path("contactmessages", contactmessages, name="contactmessages"),
path("contactmessages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
path("contact", MessageCreateView.as_view(), name="contact"),
path("messages", view_messages, name="messages"),
path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
path(
"contactmessage/<int:pk>",
ContactMessageUpdateView.as_view(),
name="contactmessage",
"message/<int:pk>",
MessageUpdateView.as_view(),
name="message",
),
path(
"contactmessage/<int:pk>/delete",
ContactMessageDeleteView.as_view(),
name="delete_contactmessage",
"message/<int:pk>/delete",
MessageDeleteView.as_view(),
name="delete_message",
),
path("imports/", imports, name="imports"),
path("imports/add", add_import, name="add_import"),
@ -134,7 +157,8 @@ urlpatterns = [
path("500/", internal_server_error, name="internal_server_error"),
path("organisme/<int:pk>/past", OrganisationDetailViewPast.as_view(), name="view_organisation_past"),
path("organisme/<int:pk>", OrganisationDetailView.as_view(), name="view_organisation"),
path("organisme/<int:pk>", OrganisationDetailView.as_view(), name="view_organisation_shortname"),
path("organisme/<int:pk>-<extra>", OrganisationDetailView.as_view(), name="view_organisation"),
path("organisme/<int:pk>-<extra>/past", OrganisationDetailViewPast.as_view(), name="view_organisation_past_fullname"),
path("organisme/<int:pk>-<extra>", OrganisationDetailView.as_view(), name="view_organisation_fullname"),
path("organisme/<int:pk>/edit", OrganisationUpdateView.as_view(), name="edit_organisation"),
@ -178,6 +202,14 @@ urlpatterns = [
re_path(r'^robots\.txt', include('robots.urls')),
path("__debug__/", include("debug_toolbar.urls")),
path("ckeditor5/", include('django_ckeditor_5.urls')),
path(
"sitemap.xml",
cache_page(86400)(sitemap),
{"sitemaps": sitemaps},
name="django.contrib.sitemaps.views.sitemap",
),
path("cache/clear", clear_cache, name="clear_cache"),
]
if settings.DEBUG:

View File

@ -0,0 +1,114 @@
from agenda_culturel.models import ReferenceLocation
import re
import unicodedata
class PlaceGuesser:
def __init__(self):
self.__citynames = list(ReferenceLocation.objects.values_list("name__lower__unaccent", "name")) + [("clermont-fd", "Clermont-Ferrand"), ("aurillac", "Aurillac"), ("montlucon", "Montluçon"), ("montferrand", "Clermont-Ferrand")]
self.__citynames = [(x[0].replace("-", " "), x[1]) for x in self.__citynames]
def __remove_accents(self, input_str):
if input_str is None:
return None
nfkd_form = unicodedata.normalize("NFKD", input_str)
return "".join([c for c in nfkd_form if not unicodedata.combining(c)])
def __guess_is_address(self, part):
toponyms = ["bd", "rue", "avenue", "place", "boulevard", "allee", ]
part = part.strip()
if re.match(r'^[0-9]', part):
return True
elems = part.split(" ")
return any([self.__remove_accents(e.lower()) in toponyms for e in elems])
def __clean_address(self, addr):
toponyms = ["bd", "rue", "avenue", "place", "boulevard", "allée", "bis", "ter", "ZI"]
for t in toponyms:
addr = re.sub(" " + t + " ", " " + t + " ", addr, flags=re.IGNORECASE)
return addr
def __guess_city_name(self, part):
part = part.strip().replace(" - ", "-")
if len(part) == 0:
return None
part = self.__remove_accents(part.lower()).replace("-", " ")
match = [x[1] for x in self.__citynames if x[0] == part]
if len(match) > 0:
return match[0]
else:
return None
def __guess_city_name_postcode(self, part):
with_pc = re.search(r'^(.*)(([0-9][ ]*){5})(.*)$', part)
if with_pc:
p1 = self.__guess_city_name(with_pc.group(1).strip())
postcode = with_pc.group(2).replace(" ", "")
p2 = self.__guess_city_name(with_pc.group(4).strip())
return postcode, p2, p1
else:
return None, self.__guess_city_name(part), None
def __guess_name_address(self, part):
with_num = re.search(r'^(([^0-9])+)([0-9]+)(.*)', part)
if with_num:
name = with_num.group(1)
return name, part[len(name):]
else:
return "", part
def guess_address_elements(self, alias):
parts = re.split(r'[,/à]', alias)
parts = [p1 for p1 in [p.strip() for p in parts] if p1 != "" and p1.lower() != "france"]
name = ""
address = ""
postcode = ""
city = ""
possible_city = ""
oparts = []
for part in parts:
p, c, possible_c = self.__guess_city_name_postcode(part)
if not possible_c is None:
possible_city = possible_c
if not c is None and city == "":
city = c
if not p is None and postcode == "":
postcode = p
if p is None and c is None:
oparts.append(part)
if city == "" and possible_city != "":
city = possible_city
else:
if len(oparts) == 0 and not possible_city != "":
oparts = [possible_city]
if city == "":
alias_simple = self.__remove_accents(alias.lower()).replace("-", " ")
mc = [x[1] for x in self.__citynames if alias_simple.endswith(" " + x[0])]
if len(mc) == 1:
city = mc[0]
if len(oparts) > 0:
if not self.__guess_is_address(oparts[0]):
name = oparts[0]
address = ", ".join(oparts[1:])
else:
name, address = self.__guess_name_address(", ".join(oparts))
address = self.__clean_address(address)
if name == "" and possible_city != "" and possible_city != city:
name = possible_city
return name, address, postcode, city

View File

@ -1,6 +1,6 @@
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic.edit import CreateView, UpdateView, DeleteView, ModelFormMixin
from django.contrib.auth.mixins import (
LoginRequiredMixin,
UserPassesTestMixin,
@ -10,6 +10,12 @@ from django import forms
from django.http import Http404
from django.contrib.postgres.search import SearchQuery, SearchHeadline
from django.utils.safestring import mark_safe
from django.utils.decorators import method_decorator
from honeypot.decorators import check_honeypot
from .utils import PlaceGuesser
import hashlib
from django.core.cache import cache
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
@ -37,13 +43,14 @@ from .forms import (
EventModerateForm,
TagForm,
TagRenameForm,
ContactMessageForm,
MessageForm,
MessageEventForm,
)
from .filters import (
EventFilter,
EventFilterAdmin,
ContactMessagesFilterAdmin,
MessagesFilterAdmin,
SimpleSearchEventFilter,
SearchEventFilter,
DuplicatedEventsFilter,
@ -55,7 +62,7 @@ from .models import (
Category,
Tag,
StaticContent,
ContactMessage,
Message,
BatchImportation,
DuplicatedEvents,
RecurrentImport,
@ -69,7 +76,7 @@ from django.utils import timezone
from django.utils.html import escape
from datetime import date, timedelta
from django.utils.timezone import datetime
from django.db.models import Q, Subquery, OuterRef, Count, F, Func
from django.db.models import Q, Subquery, OuterRef, Count, F, Func, BooleanField, ExpressionWrapper
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -293,7 +300,7 @@ def update_from_source(request, pk):
if url is None:
messages.warning(request, _("The event cannot be updated because the import process is not available for the referenced sources."))
else:
import_events_from_url.delay(url, None, True)
import_events_from_url.delay(url, None, None, True, user_id=request.user.pk if request.user else None)
messages.success(request, _("The event update has been queued and will be completed shortly."))
return HttpResponseRedirect(event.get_absolute_url())
@ -313,6 +320,10 @@ class EventUpdateView(
kwargs["is_simple_cloning"] = self.is_simple_cloning
return kwargs
def form_valid(self, form):
form.instance.set_processing_user(self.request.user)
return super().form_valid(form)
def get_initial(self):
self.is_cloning = "clone" in self.request.path.split('/')
self.is_simple_cloning = "simple-clone" in self.request.path.split('/')
@ -328,6 +339,7 @@ class EventUpdateView(
obj.save()
result["other_versions"] = obj.other_versions
result["status"] = Event.STATUS.PUBLISHED
result["cloning"] = True
if self.is_simple_cloning:
result["other_versions"] = None
@ -349,21 +361,27 @@ class EventModerateView(
permission_required = "agenda_culturel.change_event"
template_name = "agenda_culturel/event_form_moderate.html"
form_class = EventModerateForm
success_message = _("The event has been successfully moderated.")
def get_success_message(self, cleaned_data):
return mark_safe(_('The event <a href="{}">{}</a> has been moderated with success.').format(self.object.get_absolute_url(), self.object.title))
def is_moderate_next(self):
return "moderate-next" in self.request.path.split('/')
return "after" in self.request.path.split('/')
def is_starting_moderation(self):
return not "pk" in self.kwargs
def is_moderation_from_date(self):
return "m" in self.kwargs and "y" in self.kwargs and "d" in self.kwargs
def get_next_event(self, start_day, start_time):
def get_next_event(start_day, start_time):
# select non moderated events
qs = Event.objects.filter(moderated_date__isnull=True)
# select events after the current one
if start_time:
qs = qs.filter(Q(start_day__gt=start_day)|(Q(start_day=start_day) & (Q(start_time__isnull=True)|Q(start_time__gte=start_time))))
qs = qs.filter(Q(start_day__gt=start_day)|(Q(start_day=start_day) & (Q(start_time__isnull=True)|Q(start_time__gt=start_time))))
else:
qs = qs.filter(start_day__gte=start_day)
@ -384,19 +402,15 @@ class EventModerateView(
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.is_moderate_next():
context['pred'] = self.kwargs["pk"]
context['pred'] = self.kwargs["pred"]
return context
def get_object(self, queryset=None):
if self.is_starting_moderation():
now = datetime.now()
return self.get_next_event(now.date(), now.time())
return EventModerateView.get_next_event(now.date(), now.time())
else:
result = super().get_object(queryset)
if self.is_moderate_next():
return self.get_next_event(result.start_day, result.start_time)
else:
return result
return super().get_object(queryset)
def post(self, request, *args, **kwargs):
try:
@ -408,15 +422,12 @@ class EventModerateView(
def form_valid(self, form):
form.instance.set_no_modification_date_changed()
form.instance.set_in_moderation_process()
form.instance.set_processing_user(self.request.user)
return super().form_valid(form)
def get_success_url(self):
if 'save_and_next' in self.request.POST:
return reverse_lazy("moderate_event_next", args=[self.object.pk])
elif 'save_and_create_local' in self.request.POST:
return reverse_lazy("clone_edit", args=[self.object.pk])
elif 'save_and_edit' in self.request.POST:
return reverse_lazy("edit_event", args=[self.object.pk])
elif 'save_and_edit_local' in self.request.POST:
return reverse_lazy("edit_event", args=[self.object.get_local_version().pk])
else:
@ -435,6 +446,30 @@ def error_next_event(request, pk):
{"pk": pk, "object": obj},
)
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.change_event")
def moderate_event_next(request, pk):
# current event
obj = Event.objects.filter(pk=pk).first()
start_day = obj.start_day
start_time = obj.start_time
next_obj = EventModerateView.get_next_event(start_day, start_time)
if next_obj is None:
return render(
request,
"agenda_culturel/event_next_error_message.html",
{"pk": pk, "object": obj},
)
else:
return HttpResponseRedirect(reverse_lazy("moderate_event_step", args=[next_obj.pk, obj.pk]))
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.change_event")
def moderate_from_date(request, y, m, d):
d = date(y, m, d)
obj = EventModerateView.get_next_event(d, None)
return HttpResponseRedirect(reverse_lazy("moderate_event", args=[obj.pk]))
class EventDeleteView(
@ -446,9 +481,12 @@ class EventDeleteView(
success_message = _("The event has been successfully deleted.")
class EventDetailView(UserPassesTestMixin, DetailView):
class EventDetailView(UserPassesTestMixin, DetailView, ModelFormMixin):
model = Event
form_class = MessageEventForm
template_name = "agenda_culturel/page-event.html"
queryset = Event.objects.select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative").prefetch_related("message_set")
def test_func(self):
return (
@ -458,6 +496,8 @@ class EventDetailView(UserPassesTestMixin, DetailView):
def get_object(self):
o = super().get_object()
logger.warning(">>>> details")
o.download_missing_image()
y = self.kwargs["year"]
m = self.kwargs["month"]
d = self.kwargs["day"]
@ -465,6 +505,30 @@ class EventDetailView(UserPassesTestMixin, DetailView):
obj.set_current_date(date(y, m, d))
return obj
def get_success_url(self):
return self.get_object().get_absolute_url() + "#chronology"
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
message = form.save(commit=False)
message.user = self.request.user
message.related_event = self.get_object()
message.subject = _("Comment")
message.spam = False
message.closed = True
message.save()
return super().form_valid(form)
@login_required(login_url="/accounts/login/")
@ -518,31 +582,16 @@ class EventCreateView(SuccessMessageMixin, CreateView):
if form.cleaned_data['simple_cloning']:
form.instance.set_skip_duplicate_check()
if form.cleaned_data['cloning']:
form.instance.set_in_moderation_process()
form.instance.import_sources = None
form.instance.set_processing_user(self.request.user)
return super().form_valid(form)
def import_from_details(request):
form = EventForm(request.POST, is_authenticated=request.user.is_authenticated)
if form.is_valid():
new_event = form.save()
if request.user.is_authenticated:
messages.success(request, _("The event is saved."))
return HttpResponseRedirect(new_event.get_absolute_url())
else:
messages.success(
request,
_(
"The event has been submitted and will be published as soon as it has been validated by the moderation team."
),
)
return HttpResponseRedirect(reverse("home"))
else:
return render(
request, "agenda_culturel/event_form.html", context={"form": form}
)
# A class to evaluate the URL according to the existing events and the authentification
# level of the user
@ -640,7 +689,7 @@ def import_from_urls(request):
request,
_('Integrating {} url(s) into our import process.').format(len(ucat))
)
import_events_from_urls.delay(ucat)
import_events_from_urls.delay(ucat, user_id=request.user.pk if request.user else None)
return HttpResponseRedirect(reverse("thank_you"))
else:
return HttpResponseRedirect(reverse("home"))
@ -690,7 +739,7 @@ def import_from_url(request):
request,
_('Integrating {} into our import process.').format(uc.url)
)
import_events_from_url.delay(uc.url, uc.cat, uc.tags)
import_events_from_url.delay(uc.url, uc.cat, uc.tags, user_id=request.user.pk if request.user else None)
return HttpResponseRedirect(reverse("thank_you"))
@ -708,7 +757,7 @@ def export_event_ical(request, year, month, day, pk):
events = list()
events.append(event)
cal = Event.export_to_ics(events)
cal = Event.export_to_ics(events, request)
response = HttpResponse(content_type="text/calendar")
response.content = cal.to_ical().decode("utf-8").replace("\r\n", "\n")
@ -718,14 +767,17 @@ def export_event_ical(request, year, month, day, pk):
return response
def export_ical(request):
now = date.today()
request = EventFilter.set_default_values(request)
filter = EventFilter(request.GET, queryset=get_event_qs(request), request=request)
calendar = CalendarList(now + timedelta(days=-7), now + timedelta(days=+60), filter)
ical = calendar.export_to_ics()
id_cache = hashlib.md5(filter.get_url().encode("utf8")).hexdigest()
ical = cache.get(id_cache)
if not ical:
calendar = CalendarList(now + timedelta(days=-7), now + timedelta(days=+60), filter)
ical = calendar.export_to_ics(request)
cache.set(id_cache, ical, 3600) # 1 heure
response = HttpResponse(content_type="text/calendar")
response.content = ical.to_ical().decode("utf-8").replace("\r\n", "\n")
@ -736,15 +788,19 @@ def export_ical(request):
return response
class ContactMessageCreateView(SuccessMessageMixin, CreateView):
model = ContactMessage
template_name = "agenda_culturel/contactmessage_create_form.html"
form_class = ContactMessageForm
@method_decorator(check_honeypot, name='post')
class MessageCreateView(SuccessMessageMixin, CreateView):
model = Message
template_name = "agenda_culturel/message_create_form.html"
form_class = MessageForm
success_url = reverse_lazy("home")
success_message = _("Your message has been sent successfully.")
def __init__(self, *args, **kwargs):
self.event = None
super().__init__(*args, **kwargs)
def get_form(self, form_class=None):
if form_class is None:
form_class = self.get_form_class()
@ -753,39 +809,48 @@ class ContactMessageCreateView(SuccessMessageMixin, CreateView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["event"] = self.event
if self.request.user.is_authenticated:
kwargs["internal"] = True
return kwargs
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
def get_initial(self):
result = super().get_initial()
if "pk" in self.kwargs:
self.event = get_object_or_404(Event, pk=self.kwargs["pk"])
result["related_event"] = self.event
result["subject"] = _('Reporting the event {} on {}').format(self.event.title, self.event.start_day)
else:
result["related_event"] = None
return result
class ContactMessageDeleteView(SuccessMessageMixin, DeleteView):
model = ContactMessage
class MessageDeleteView(SuccessMessageMixin, DeleteView):
model = Message
success_message = _(
"The contact message has been successfully deleted."
)
success_url = reverse_lazy("contactmessages")
success_url = reverse_lazy("messages")
class ContactMessageUpdateView(
class MessageUpdateView(
SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView
):
model = ContactMessage
permission_required = "agenda_culturel.change_contactmessage"
template_name = "agenda_culturel/contactmessage_moderation_form.html"
model = Message
permission_required = "agenda_culturel.change_message"
template_name = "agenda_culturel/message_moderation_form.html"
fields = ("spam", "closed", "comments")
success_message = _(
"The contact message properties has been successfully modified."
)
success_url = reverse_lazy("contactmessages")
success_url = reverse_lazy("messages")
def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""
@ -822,23 +887,27 @@ def activite(request):
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.view_event")
def administration(request):
nb_mod_days = 21
nb_classes = 4
today = date.today()
start_time = datetime.now().time()
# get information about recent modifications
days = [date.today()]
days = [today]
for i in range(0, 2):
days.append(days[-1] + timedelta(days=-1))
daily_modifications = Event.get_count_modifications([(d, 1) for d in days])
# get last created events
events = Event.objects.all().order_by("-created_date")[:5]
events = Event.objects.all().order_by("-created_date").select_related("exact_location", "category")[:5]
# get last batch imports
batch_imports = BatchImportation.objects.all().order_by("-created_date")[:5]
batch_imports = BatchImportation.objects.all().select_related("recurrentImport").order_by("-created_date")[:5]
# get info about batch information
newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by(
"-created_date"
)
).select_related("recurrentImport")
imported_events = RecurrentImport.objects.annotate(
last_run_status=Subquery(newest.values("status")[:1])
)
@ -854,13 +923,41 @@ def administration(request):
.count())
nb_all = imported_events.count()
window_end = today + timedelta(days=nb_mod_days)
# get all non moderated events
nb_not_moderated = Event.objects.filter(~Q(status=Event.STATUS.TRASH)). \
filter(Q(start_day__gte=today)&Q(start_day__lte=window_end)). \
filter(
Q(other_versions__isnull=True) |
Q(other_versions__representative=F('pk')) |
Q(other_versions__representative__isnull=True)).values("start_day").\
annotate(not_moderated=Count("start_day", filter=Q(moderated_date__isnull=True))). \
annotate(nb_events=Count("start_day")). \
order_by("start_day").values("not_moderated", "nb_events", "start_day")
max_not_moderated = max([x["not_moderated"] for x in nb_not_moderated])
if max_not_moderated == 0:
max_not_moderated = 1
nb_not_moderated_dict = dict([(x["start_day"], (x["not_moderated"], x["nb_events"])) for x in nb_not_moderated])
# add missing dates
date_list = [today + timedelta(days=x) for x in range(0, nb_mod_days)]
nb_not_moderated = [{"start_day": d,
"is_today": d == today,
"nb_events": nb_not_moderated_dict[d][1] if d in nb_not_moderated_dict else 0,
"not_moderated": nb_not_moderated_dict[d][0] if d in nb_not_moderated_dict else 0} for d in date_list]
nb_not_moderated = [ x | { "note": 0 if x["not_moderated"] == 0 else int((nb_classes - 1) * x["not_moderated"] / max_not_moderated) + 1 } for x in nb_not_moderated]
nb_not_moderated = [nb_not_moderated[x:x + 7] for x in range(0, len(nb_not_moderated), 7)]
return render(
request,
"agenda_culturel/administration.html",
{"daily_modifications": daily_modifications,
"events": events, "batch_imports": batch_imports,
"nb_failed": nb_failed, "nb_canceled": nb_canceled,
"nb_running": nb_running, "nb_all": nb_all},
"nb_running": nb_running, "nb_all": nb_all,
"nb_not_moderated": nb_not_moderated},
)
@ -889,15 +986,15 @@ def recent(request):
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.view_contactmessage")
def contactmessages(request):
filter = ContactMessagesFilterAdmin(
request.GET, queryset=ContactMessage.objects.all().order_by("-date")
@permission_required("agenda_culturel.view_message")
def view_messages(request):
filter = MessagesFilterAdmin(
request.GET, queryset=Message.objects.all().order_by("-date")
)
paginator = PaginatorFilter(filter, 10, request)
page = request.GET.get("page")
nb_spams = ContactMessage.objects.filter(spam=True).count()
nb_spams = Message.objects.filter(spam=True).count()
try:
response = paginator.page(page)
@ -908,24 +1005,24 @@ def contactmessages(request):
return render(
request,
"agenda_culturel/contactmessages.html",
"agenda_culturel/messages.html",
{"filter": filter, "nb_spams": nb_spams, "paginator_filter": response},
)
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.view_contactmessage")
@permission_required("agenda_culturel.view_message")
def delete_cm_spam(request):
if request.method == "POST":
ContactMessage.objects.filter(spam=True).delete()
Message.objects.filter(spam=True).delete()
messages.success(request, _("Spam has been successfully deleted."))
return HttpResponseRedirect(reverse_lazy("contactmessages"))
return HttpResponseRedirect(reverse_lazy("messages"))
else:
nb_msgs = ContactMessage.objects.values('spam').annotate(total=Count('spam'))
nb_msgs = Message.objects.values('spam').annotate(total=Count('spam'))
nb_total = sum([nb["total"] for nb in nb_msgs])
nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]])
cancel_url = reverse_lazy("contactmessages")
cancel_url = reverse_lazy("messages")
return render(
request,
"agenda_culturel/delete_spams_confirm.html",
@ -1481,6 +1578,7 @@ def set_duplicate(request, year, month, day, pk):
event.other_versions is None
or event.other_versions != e.other_versions
)
and e.status != Event.STATUS.TRASH
]
form = SelectEventInList(events=others)
@ -1856,6 +1954,7 @@ class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateVi
class PlaceFromEventCreateView(PlaceCreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["event"] = self.event
@ -1866,6 +1965,14 @@ class PlaceFromEventCreateView(PlaceCreateView):
self.event = get_object_or_404(Event, pk=self.kwargs["pk"])
if self.event.location and "add" in self.request.GET:
initial["aliases"] = [self.event.location]
guesser = PlaceGuesser()
name, address, postcode, city = guesser.guess_address_elements(self.event.location)
initial["name"] = name
initial["address"] = address
initial["postcode"] = postcode
initial["city"] = city
initial["location"] = ""
return initial
def form_valid(self, form):
@ -2125,3 +2232,15 @@ def delete_tag(request, t):
"agenda_culturel/tag_confirm_delete_by_name.html",
{"tag": t, "nb": nb, "nbi": nbi, "cancel_url": cancel_url, "obj": obj},
)
def clear_cache(request):
if request.method == "POST":
cache.clear()
messages.success(request, _("Cache successfully cleared."))
return HttpResponseRedirect(reverse_lazy("administration"))
else:
return render(
request,
"agenda_culturel/clear_cache.html",
)

View File

@ -42,4 +42,5 @@ django-location-field==2.7.3
django-robots==6.1
django-debug-toolbar==4.4.6
django-cache-cleaner==0.1.0
emoji==2.14.0
emoji==2.14.0
django-honeypot==1.2.1

View File

@ -1,5 +1,5 @@
from agenda_culturel.models import ContactMessage
from agenda_culturel.models import Message
def run():
ContactMessage.objects.all().update(spam=True)
Message.objects.all().update(spam=True)