Compare commits

..

No commits in common. "main" and "filter-import-comedie" have entirely different histories.

87 changed files with 843 additions and 3654 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 --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)
* ```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)
* 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,11 +5,10 @@ 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 fonts-symbola \
apt-get install --no-install-recommends -y build-essential libpq-dev gettext chromium-driver gdal-bin \
&& rm -rf /var/lib/apt/lists/*
COPY src/requirements.txt ./requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \

View File

@ -1,43 +0,0 @@
#!/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,10 +73,6 @@ 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
@ -89,4 +85,7 @@ 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,
Message,
ContactMessage,
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(Message)
admin.site.register(ContactMessage)
admin.site.register(ReferenceLocation)
admin.site.register(Organisation)

View File

@ -117,23 +117,6 @@ 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
@ -192,13 +175,12 @@ class IntervalInDay(DayInCalendar):
self.id = self.id + '-' + str(id)
class CalendarList:
def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=None, qs=None):
def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=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
@ -237,12 +219,9 @@ class CalendarList:
def fill_calendar_days(self):
if self.filter is None:
if self.qs is None:
from .models import Event
from .models import Event
qs = Event.objects.all()
else:
qs = self.qs
qs = Event.objects.all()
else:
qs = self.filter.qs
@ -250,7 +229,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())
qs = qs.filter(
self.events = qs.filter(
(Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime))
| (
Q(recurrence_dtend__isnull=False)
@ -264,10 +243,7 @@ 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")
qs = qs.select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative")
self.events = qs
).order_by("start_time", "title__unaccent__lower").select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative")
firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
@ -315,14 +291,14 @@ class CalendarList:
def time_intervals_list_first(self):
return self.time_intervals_list(True)
def export_to_ics(self, request):
def export_to_ics(self):
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, request)
return Event.export_to_ics(events)
class CalendarMonth(CalendarList):
def __init__(self, year, month, filter, qs=None):
def __init__(self, year, month, filter):
self.year = year
self.month = month
r = calendar.monthrange(year, month)
@ -330,7 +306,7 @@ class CalendarMonth(CalendarList):
first = date(year, month, 1)
last = date(year, month, r[1])
super().__init__(first, last, filter, qs)
super().__init__(first, last, filter)
def get_month_name(self):
return self.firstdate.strftime("%B")
@ -343,14 +319,14 @@ class CalendarMonth(CalendarList):
class CalendarWeek(CalendarList):
def __init__(self, year, week, filter, qs=None):
def __init__(self, year, week, filter):
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, qs)
super().__init__(first, last, filter)
def next_week(self):
return self.firstdate + timedelta(days=7)
@ -360,8 +336,8 @@ class CalendarWeek(CalendarList):
class CalendarDay(CalendarList):
def __init__(self, date, filter=None, qs=None):
super().__init__(date, date, filter=filter, qs=qs, exact=True)
def __init__(self, date, filter=None):
super().__init__(date, date, filter, exact=True)
def get_events(self):
return self.calendar_days_list()[0].events

View File

@ -6,8 +6,7 @@ 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
@ -148,8 +147,6 @@ 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
@ -176,11 +173,6 @@ 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)
@ -256,23 +248,6 @@ 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
@ -314,7 +289,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, user_id=None, email=None, comments=None):
def import_events_from_url(self, url, cat, tags, force=False):
from .db_importer import DBImporterEvents
from agenda_culturel.models import RecurrentImport, BatchImportation
from agenda_culturel.models import Event, Category
@ -323,7 +298,7 @@ def import_events_from_url(self, url, cat, tags, force=False, user_id=None, emai
if acquired:
logger.info("URL import: {}".format(self.request.id) + " force " + str(force))
logger.info("URL import: {}".format(self.request.id))
# clean url
@ -348,13 +323,7 @@ def import_events_from_url(self, url, cat, tags, force=False, user_id=None, emai
# set default values
values = {}
if cat is not None:
values["category"] = cat
if tags is not None:
values["tags"] = tags
if email is not None:
values["email"] = email
if comments is not None:
values["comments"] = comments
values = {"category": cat, "tags": tags}
# get event
events = u2e.process(
@ -366,7 +335,7 @@ def import_events_from_url(self, url, cat, tags, force=False, user_id=None, emai
json_events = json.dumps(events, default=str)
# import events (from json)
success, error_message = importer.import_events(json_events, user_id)
success, error_message = importer.import_events(json_events)
# finally, close task
close_import_task(self.request.id, success, error_message, importer)
@ -383,14 +352,14 @@ def import_events_from_url(self, url, cat, tags, force=False, user_id=None, emai
@app.task(base=ChromiumTask, bind=True)
def import_events_from_urls(self, urls_cat_tags, user_id=None, email=None, comments=None):
def import_events_from_urls(self, urls_cat_tags):
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, user_id=user_id, email=email, comments=comments)
import_events_from_url.delay(url, cat, tags)
app.conf.beat_schedule = {
@ -399,10 +368,6 @@ 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,7 +11,6 @@ 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()
@ -35,10 +34,9 @@ class DBImporterEvents:
def get_nb_removed_events(self):
return self.nb_removed
def import_events(self, json_structure, user_id=None):
def import_events(self, json_structure):
print(json_structure)
self.init_result_properties()
self.user_id = user_id
try:
structure = json.loads(json_structure)
@ -97,7 +95,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, user_id=self.user_id
self.event_objects, remove_missing_from_source=self.url
)
def is_valid_event_structure(self, event):

View File

@ -44,7 +44,7 @@ from .models import (
Tag,
Event,
Category,
Message,
ContactMessage,
DuplicatedEvents
)
@ -137,11 +137,7 @@ 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")
if not isinstance(d, str) or not isinstance(p, ReferenceLocation):
return parent
p = p.location
p = self.get_cleaned_data("position").location
return parent.exclude(exact_location=False).filter(exact_location__location__distance_lt=(p, D(km=d)))
def get_url(self):
@ -192,7 +188,6 @@ class EventFilter(django_filters.FilterSet):
def get_cleaned_data(self, name):
try:
return self.form.cleaned_data[name]
except AttributeError:
@ -314,13 +309,6 @@ class EventFilter(django_filters.FilterSet):
else:
return str(self.get_cleaned_data("position")) + ' (' + str(self.get_cleaned_data("radius")) + ' km)'
def is_filtered_by_position_radius(self):
return not self.get_cleaned_data("position") is None and not self.get_cleaned_data("radius") is None
def get_url_add_suggested_position(self, location):
result = self.request.get_full_path()
return result + ('&' if '?' in result else '?') + 'position=' + str(location.pk) + "&radius=" + str(location.suggested_distance)
class EventFilterAdmin(django_filters.FilterSet):
status = django_filters.MultipleChoiceFilter(
@ -361,27 +349,21 @@ class EventFilterAdmin(django_filters.FilterSet):
fields = ["status"]
class MessagesFilterAdmin(django_filters.FilterSet):
class ContactMessagesFilterAdmin(django_filters.FilterSet):
closed = django_filters.MultipleChoiceFilter(
label=_("Status"),
label="Status",
choices=((True, _("Closed")), (False, _("Open"))),
widget=forms.CheckboxSelectMultiple,
)
spam = django_filters.MultipleChoiceFilter(
label=_("Spam"),
label="Spam",
choices=((True, _("Spam")), (False, _("Non spam"))),
widget=forms.CheckboxSelectMultiple,
)
message_type = django_filters.MultipleChoiceFilter(
label=_("Type"),
choices=Message.TYPE.choices,
widget=forms.CheckboxSelectMultiple,
)
class Meta:
model = Message
fields = ["closed", "spam", "message_type"]
model = ContactMessage
fields = ["closed", "spam"]
class SimpleSearchEventFilter(django_filters.FilterSet):

View File

@ -13,21 +13,16 @@ from django.forms import (
BooleanField,
HiddenInput,
ModelChoiceField,
EmailField
)
from django.forms import formset_factory
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
from .utils import PlaceGuesser
from .models import (
Event,
RecurrentImport,
CategorisationRule,
Place,
Category,
Tag,
Message
Tag
)
from django.conf import settings
from django.core.files import File
@ -36,6 +31,7 @@ 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
@ -77,7 +73,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] + [(GroupFormMixin.FieldGroup("other", _("Other")), self.get_no_group_fields())]
return [(g, self.get_fields_in_group(g)) for g in self.groups] + [(None, self.get_no_group_fields())]
def clean(self):
result = super().clean()
@ -128,42 +124,7 @@ class TagRenameForm(Form):
def is_force(self):
return "force" in self.fields and self.cleaned_data["force"] == True
class SimpleContactForm(GroupFormMixin, Form):
email = EmailField(
label=_("Your email"),
help_text=_("Your email address"),
max_length=254,
required=False
)
comments = CharField(
label=_("Comments"),
help_text=_("Your message for the moderation team (comments, clarifications, requests...)"),
widget=Textarea,
max_length=2048,
required=False
)
def __init__(self, *args, **kwargs):
is_authenticated = "is_authenticated" in kwargs and kwargs["is_authenticated"]
super().__init__(*args, **kwargs)
if not is_authenticated:
self.add_group('communication',
_('Receive notification of publication or leave a message for moderation'),
maskable=True,
default_masked=True)
self.fields["email"].group_id = 'communication'
self.fields["comments"].group_id = 'communication'
else:
del self.fields["email"]
del self.fields["comments"]
class URLSubmissionForm(GroupFormMixin, Form):
class URLSubmissionForm(Form):
required_css_class = 'required'
url = URLField(max_length=512)
@ -181,20 +142,11 @@ class URLSubmissionForm(GroupFormMixin, Form):
)
def __init__(self, *args, **kwargs):
is_authenticated = kwargs.pop("is_authenticated", False)
super().__init__(*args, **kwargs)
self.fields["tags"].choices = Tag.get_tag_groups(all=True)
self.add_group('event', _('Event'))
self.fields["url"].group_id = 'event'
self.fields["category"].group_id = 'event'
self.fields["tags"].group_id = 'event'
class URLSubmissionFormWithContact(SimpleContactForm, URLSubmissionForm):
pass
URLSubmissionFormSet = formset_factory(URLSubmissionForm, extra=9, min_num=1)
class DynamicArrayWidgetURLs(DynamicArrayWidget):
template_name = "agenda_culturel/widgets/widget-urls.html"
@ -207,20 +159,12 @@ class DynamicArrayWidgetTags(DynamicArrayWidget):
class RecurrentImportForm(ModelForm):
required_css_class = 'required'
defaultTags = MultipleChoiceField(
label=_("Tags"),
initial=None,
choices=[],
required=False
)
class Meta:
model = RecurrentImport
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["defaultTags"].choices = Tag.get_tag_groups(all=True)
widgets = {
"defaultTags": DynamicArrayWidgetTags(),
}
class CategorisationRuleImportForm(ModelForm):
@ -236,7 +180,6 @@ 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"),
@ -253,11 +196,7 @@ class EventForm(GroupFormMixin, ModelForm):
"modified_date",
"moderated_date",
"import_sources",
"image",
"moderated_by_user",
"modified_by_user",
"created_by_user",
"imported_by_user"
"image"
]
widgets = {
"start_day": TextInput(
@ -306,14 +245,7 @@ class EventForm(GroupFormMixin, ModelForm):
self.fields['end_day'].group_id = 'end'
self.fields['end_time'].group_id = 'end'
self.add_group('recurrences',
_('This is a recurring event'),
maskable=True,
default_masked=not (self.instance and
self.instance.recurrences and
self.instance.recurrences.rrules and
len(self.instance.recurrences.rrules) > 0))
self.add_group('recurrences', _('This is a recurring event'), maskable=True, default_masked=True)
self.fields['recurrences'].group_id = 'recurrences'
self.add_group('details', _('Details'))
@ -329,8 +261,6 @@ class EventForm(GroupFormMixin, ModelForm):
self.fields['local_image'].group_id = 'illustration'
self.fields['image_alt'].group_id = 'illustration'
self.add_group('urls', _('URLs'))
self.fields["reference_urls"].group_id = 'urls'
if is_authenticated:
self.add_group('meta-admin', _('Meta information'))
@ -387,9 +317,6 @@ class EventForm(GroupFormMixin, ModelForm):
self.cleaned_data['local_image'] = File(name=basename, file=open(old, "rb"))
class EventFormWithContact(SimpleContactForm, EventForm):
pass
class MultipleChoiceFieldAcceptAll(MultipleChoiceField):
def validate(self, value):
pass
@ -420,9 +347,6 @@ class EventModerateForm(ModelForm):
"exact_location",
"tags"
]
widgets = {
"status": RadioSelect
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -546,14 +470,14 @@ class FixDuplicates(Form):
class SelectEventInList(Form):
required_css_class = 'required'
event = ChoiceField(label=_('Event'))
event = ChoiceField()
def __init__(self, *args, **kwargs):
events = kwargs.pop("events", None)
super().__init__(*args, **kwargs)
self.fields["event"].choices = [
(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
(e.pk, str(e.start_day) + " " + e.title + ((", " + e.location) if e.location else "")) for e in events
]
@ -601,17 +525,17 @@ class MergeDuplicates(Form):
'<li><a href="' + e.get_absolute_url() + '">' + e.title + "</a></li>"
)
result += (
"<li>Création&nbsp;: " + localize(e.created_date) + "</li>"
"<li>Création&nbsp;: " + localize(localtime(e.created_date)) + "</li>"
)
result += (
"<li>Dernière modification&nbsp;: "
+ localize(e.modified_date)
+ localize(localtime(e.modified_date))
+ "</li>"
)
if e.imported_date:
result += (
"<li>Dernière importation&nbsp;: "
+ localize(e.imported_date)
+ localize(localtime(e.imported_date))
+ "</li>"
)
result += "</ul>"
@ -662,7 +586,7 @@ class MergeDuplicates(Form):
result += '<input id="' + id + '" name="' + key + '"'
if key in MergeDuplicates.checkboxes_fields:
result += ' type="checkbox"'
if checked and value in checked:
if value in checked:
result += " checked"
else:
result += ' type="radio"'
@ -694,7 +618,7 @@ class MergeDuplicates(Form):
result = []
for s in selected:
for e in self.duplicates.get_duplicated():
if e.pk == s:
if e.pk == selected:
result.append(e)
break
return result
@ -778,7 +702,7 @@ class EventAddPlaceForm(Form):
return self.instance
class PlaceForm(GroupFormMixin, ModelForm):
class PlaceForm(ModelForm):
required_css_class = 'required'
apply_to_all = BooleanField(
@ -794,70 +718,13 @@ class PlaceForm(GroupFormMixin, 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):
result = ('<div class="grid"><div>'
return mark_safe(
'<div class="grid"><div>'
+ super().as_p()
+ '''</div><div><div class="map-widget">
<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div>
<p>Cliquez pour ajuster la position GPS</p></div>
<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)
+ '</div><div><div class="map-widget">'
+ '<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div><p>Cliquez pour ajuster la position GPS</p></div></div></div>'
)
def apply(self):
return self.cleaned_data.get("apply_to_all")
class MessageForm(ModelForm):
class Meta:
model = Message
fields = ["subject", "name", "email", "message", "related_event"]
widgets = {"related_event": HiddenInput(), "user": HiddenInput() }
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event", False)
self.internal = kwargs.pop("internal", False)
super().__init__(*args, **kwargs)
self.fields['related_event'].required = False
if self.internal:
self.fields.pop("name")
self.fields.pop("email")
class MessageEventForm(ModelForm):
class Meta:
model = Message
fields = ["message"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["message"].label = _("Add a comment")

View File

@ -3,12 +3,6 @@ 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
@ -19,27 +13,10 @@ 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(
@ -65,7 +42,4 @@ class CExtractor(TwoStepsExtractor):
event["published"] = published
self.add_event(default_values, **event)
else:
logger.warning("cannot find any event in page")

View File

@ -106,16 +106,6 @@ class CExtractor(TwoStepsExtractor):
description = soup.select("#descspec")
if description and len(description) > 0:
description = description[0].get_text().replace("Lire plus...", "")
# on ajoute éventuellement les informations complémentaires
d_suite = ""
for d in ["#typespec", "#dureespec", "#lieuspec", ".lkuncontdroitespec"]:
comp_desc = soup.select(d)
if comp_desc and len(comp_desc) > 0:
for desc in comp_desc:
d_suite += "\n\n" + desc.get_text()
if d_suite != "":
description += "\n\n> Informations complémentaires:" + d_suite
else:
description = None

View File

@ -1,91 +0,0 @@
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, noimage=True):
def __init__(self, pause=True):
super().__init__()
self.pause = pause
self.options = Options()
@ -78,31 +78,17 @@ 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")
if noimage:
self.options.add_experimental_option(
"prefs", {
# block image loading
"profile.managed_default_content_settings.images": 2,
}
)
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

@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
from datetime import datetime, time, date, timedelta
import re
import unicodedata
from django.utils import timezone
@ -49,7 +49,7 @@ class Extractor(ABC):
return i + 1
return None
def parse_french_date(text, default_year=None):
def parse_french_date(text):
# format NomJour Numero Mois Année
m = re.search(
"[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text
@ -73,15 +73,8 @@ class Extractor(ABC):
month = int(m.group(2))
year = m.group(3)
else:
# 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
# TODO: consolider les cas non satisfaits
return None
if month is None:
return None
@ -200,8 +193,6 @@ class Extractor(ABC):
"published": published,
"image": image,
"image_alt": image_alt,
"email": self.default_value_if_exists(default_values, "email"),
"comments": self.default_value_if_exists(default_values, "comments"),
}
# TODO: pourquoi url_human et non reference_url
if url_human is not None:
@ -249,28 +240,6 @@ class Extractor(ABC):
from .extractor_ggcal_link import GoogleCalendarLinkEventExtractor
if single_event:
return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]
return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor()]
else:
return [ICALExtractor(), FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]
# A class that only produce a not found event
class EventNotFoundExtractor(Extractor):
def extract(
self, content, url, url_human=None, default_values=None, published=False
):
self.set_header(url)
self.clear_events()
self.add_event(default_values, "événement sans titre depuis " + url,
None, timezone.now().date(), None,
"l'import a échoué, la saisie doit se faire manuellement à partir de l'url source " + url,
[], [url], published=False, url_human=url)
return self.get_structure()
def clean_url(url):
return url
return [ICALExtractor(), FacebookEventExtractor(), GoogleCalendarLinkEventExtractor()]

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,13 +264,9 @@ class TwoStepsExtractorNoPause(TwoStepsExtractor):
only_future=True,
ignore_404=True
):
if hasattr(self.downloader, "pause"):
pause = self.downloader.pause
else:
pause = False
pause = self.downloader.pause
self.downloader.pause = False
result = super().extract(content, url, url_human, default_values, published, only_future, ignore_404)
self.downloader.pause = pause
return result
return result

View File

@ -1,11 +1,6 @@
from .downloader import *
from .extractor import *
import logging
logger = logging.getLogger(__name__)
class URL2Events:
def __init__(
@ -34,9 +29,8 @@ class URL2Events:
else:
# if the extractor is not defined, use a list of default extractors
for e in Extractor.get_default_extractors(self.single_event):
logger.warning('Extractor::' + type(e).__name__)
e.set_downloader(self.downloader)
events = e.extract(content, url, url_human, default_values, published)
if events is not None and len(events) > 0:
if events is not None:
return events
return None

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):
Place = apps.get_model("agenda_culturel", "Place")
places = Place.objects.values("location", "location_pt").all()
places = Place.objects.all()
for p in places:
l = p.location.split(',')
@ -13,15 +13,14 @@ 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(update_fields=["location_pt"])
p.save()
def reverse_coord_format(apps, schema_editor):
Place = apps.get_model("agenda_culturel", "Place")
places = Place.objects.values("location", "location_pt").all()
places = Place.objects.all()
for p in places:
p.location = ','.join([p.location_pt[1], p.location_pt[0]])
p.save(update_fields=["location"])
p.save()

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.9 on 2024-11-27 18:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0119_alter_tag_options_alter_event_category_and_more'),
]
operations = [
migrations.AddField(
model_name='referencelocation',
name='suggested_distance',
field=models.IntegerField(default=None, help_text='If this distance is given, this location is part of the suggested filters.', null=True, verbose_name='Suggested distance (km)'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.9 on 2024-11-27 22:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0120_referencelocation_suggested_distance'),
]
operations = [
migrations.AddField(
model_name='contactmessage',
name='related_event',
field=models.ForeignKey(default=None, help_text='The message is associated with this event.', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='agenda_culturel.event', verbose_name='Related event'),
),
]

View File

@ -1,18 +0,0 @@
# 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

@ -1,36 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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

@ -1,21 +0,0 @@
# 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

@ -1,21 +0,0 @@
# 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

@ -1,25 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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

@ -1,57 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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

@ -1,18 +0,0 @@
# Generated by Django 4.2.9 on 2024-12-29 11:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0130_recurrentimport_forcelocation'),
]
operations = [
migrations.AddField(
model_name='message',
name='message_type',
field=models.CharField(choices=[('from_contributor', 'From contributor'), ('import_process', 'Import process'), ('contact_form', 'Contact form'), ('event_report', 'Event report')], default=None, max_length=20, null=True, verbose_name='Type'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.9 on 2024-12-29 16:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0131_message_message_type'),
]
operations = [
migrations.AlterField(
model_name='message',
name='message_type',
field=models.CharField(choices=[('from_contributor', 'From contributor'), ('import_process', 'Import process'), ('contact_form', 'Contact form'), ('event_report', 'Event report'), ('from_contrib_no_msg', 'From contributor (without message)')], default=None, max_length=20, null=True, verbose_name='Type'),
),
]

View File

@ -10,16 +10,7 @@ from colorfield.fields import ColorField
from django_ckeditor_5.fields import CKEditor5Field
from urllib.parse import urlparse
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.contrib.auth.models import User, AnonymousUser
import emoji
from django.core.files.storage import default_storage
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.template.loader import render_to_string
import uuid
import hashlib
import urllib.request
@ -29,7 +20,6 @@ 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
@ -294,10 +284,6 @@ 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
@ -412,7 +398,6 @@ class DuplicatedEvents(models.Model):
class ReferenceLocation(models.Model):
name = models.CharField(verbose_name=_("Name"), help_text=_("Name of the location"), unique=True, null=False)
location = LocationField(based_fields=["name"], zoom=12, default=Point(3.08333, 45.783329), srid=4326)
main = models.IntegerField(
@ -420,12 +405,6 @@ class ReferenceLocation(models.Model):
help_text=_("This location is one of the main locations (shown first higher values)."),
default=0,
)
suggested_distance = models.IntegerField(
verbose_name=_("Suggested distance (km)"),
help_text=_("If this distance is given, this location is part of the suggested filters."),
null=True,
default=None
)
class Meta:
verbose_name = _("Reference location")
@ -448,9 +427,8 @@ 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", "postcode", "city"], zoom=12, default=Point(3.08333, 45.783329))
location = LocationField(based_fields=["name", "address", "city"], zoom=12, default=Point(3.08333, 45.783329))
description = CKEditor5Field(
verbose_name=_("Description"),
@ -473,11 +451,6 @@ 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:
@ -563,7 +536,7 @@ class Organisation(models.Model):
return self.name
def get_absolute_url(self):
return reverse("view_organisation", kwargs={'pk': self.pk, "extra": self.name})
return reverse("view_organisation", kwargs={'pk': self.pk})
@ -578,39 +551,6 @@ 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)
@ -745,10 +685,6 @@ 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)
@ -766,6 +702,15 @@ 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)
@ -775,15 +720,6 @@ 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)
@ -821,36 +757,9 @@ class Event(models.Model):
permissions = [("set_duplicated_event", "Can set an event as duplicated")]
indexes = [
models.Index(fields=["start_day", "start_time"]),
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")
models.Index("start_time", Lower("title"), name="start_time 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 []
@ -942,27 +851,19 @@ class Event(models.Model):
def is_representative(self):
return self.other_versions is None or self.other_versions.representative == self
def download_missing_image(self):
if self.local_image and not default_storage.exists(self.local_image.name):
self.download_image()
self.save(update_fields=["local_image"])
def download_image(self):
# first download file
a = urlparse(self.image)
basename = os.path.basename(a.path)
ext = basename.split('.')[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
try:
tmpfile, _ = urllib.request.urlretrieve(self.image)
except:
return None
# if the download is ok, then create the corresponding file object
self.local_image = File(name=filename, file=open(tmpfile, "rb"))
self.local_image = File(name=basename, file=open(tmpfile, "rb"))
def add_pending_organisers(self, organisers):
self.pending_organisers = organisers
@ -988,12 +889,6 @@ 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
@ -1004,16 +899,12 @@ 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(
@ -1025,13 +916,10 @@ class Event(models.Model):
else:
return recurrences[0]
def get_image_url(self, request=None):
def get_image_url(self):
if self.local_image and hasattr(self.local_image, "url"):
try:
if request:
return request.build_absolute_uri(self.local_image.url)
else:
return self.local_image.url
return self.local_image.url
except:
pass
if self.image:
@ -1129,7 +1017,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 or not default_storage.exists(self.local_image.name)):
if self.image and not self.local_image:
self.download_image()
# remove "/" from tags
@ -1148,44 +1036,6 @@ class Event(models.Model):
if not self.category or self.category.name == Category.default_name:
CategorisationRule.apply_rules(self)
def get_contributor_message(self):
types = [Message.TYPE.FROM_CONTRIBUTOR, Message.TYPE.FROM_CONTRIBUTOR_NO_MSG]
if self.other_versions is None or self.other_versions.representative is None:
logger.warning("cas single")
return Message.objects.filter(related_event=self.pk, message_type__in=types, closed=False)
else:
logger.warning("cas multiple")
return Message.objects.filter(related_event__in=self.other_versions.get_duplicated(), message_type__in=types, closed=False).first()
def notify_if_required(self, request):
notif = False
if self.status != Event.STATUS.DRAFT:
messages = self.get_contributor_message()
logger.warning("messages: ")
logger.warning(messages)
if messages:
for message in messages:
if message and not message.closed and message.email:
# send email
context = {"sitename": Site.objects.get_current(request).name, 'event_title': self.title }
if self.status == Event.STATUS.PUBLISHED:
context["url"] = request.build_absolute_uri(self.get_absolute_url())
subject = _('Your event has been published')
body = render_to_string("agenda_culturel/emails/published.txt", context)
else:
subject = _('Your message has not been retained')
body = render_to_string("agenda_culturel/emails/retained.txt", context)
send_mail(subject, body, None, [message.email])
message.closed = True
message.save()
notif = True
return notif
def save(self, *args, **kwargs):
self.prepare_save()
@ -1219,21 +1069,6 @@ class Event(models.Model):
# first save the current object
super().save(*args, **kwargs)
# notify only if required (and request is known)
if "request" in kwargs:
self.notify_if_required(kwargs.get("request"))
# clear cache
for is_auth in [False, True]:
key = make_template_fragment_key("event_body", [is_auth, self])
cache.delete(key)
# save message if required
if self.has_message():
msg = self.get_message()
msg.related_event = self
msg.save()
# then if its a clone, update the representative
if clone:
self.other_versions.representative = self
@ -1250,8 +1085,6 @@ class Event(models.Model):
def from_structure(event_structure, import_source=None):
# organisers is a manytomany relation thus cannot be initialised before creation of the event
organisers = event_structure.pop('organisers', None)
email = event_structure.pop('email', None)
comments = event_structure.pop('comments', None)
if "category" in event_structure and event_structure["category"] is not None:
try:
@ -1281,6 +1114,8 @@ class Event(models.Model):
and event_structure["last_modified"] is not None
):
d = datetime.fromisoformat(event_structure["last_modified"])
if d.year == 2024 and d.month > 2:
logger.warning("last modified {}".format(d))
if d.tzinfo is None or d.tzinfo.utcoffset(d) is None:
d = timezone.make_aware(d, timezone.get_default_timezone())
event_structure["modified_date"] = d
@ -1328,13 +1163,7 @@ class Event(models.Model):
result = Event(**event_structure)
result.add_pending_organisers(organisers)
if email or comments:
has_comments = not comments in ["", None]
result.set_message(Message(subject=_('during import process'),
email=email,
message=comments,
closed=False,
message_type=Message.TYPE.FROM_CONTRIBUTOR if has_comments else Message.TYPE.FROM_CONTRIBUTOR_NO_MSG))
return result
@ -1486,10 +1315,9 @@ 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
@ -1510,8 +1338,6 @@ class Event(models.Model):
"category",
"tags",
]
if not no_m2m:
result += ["organisers"]
result += [
"title",
@ -1523,6 +1349,8 @@ 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:
@ -1569,19 +1397,12 @@ class Event(models.Model):
self.uuids.append(uuid)
# add possible missing sources
if other.import_sources:
if not self.import_sources:
self.import_sources = []
for source in other.import_sources:
if source not in self.import_sources:
self.import_sources.append(source)
for source in other.import_sources:
if source not in self.import_sources:
self.import_sources.append(source)
# Limitation: the given events should not be considered similar one to another...
def import_events(events, remove_missing_from_source=None, user_id=None):
user = None
if user_id:
user = User.objects.filter(pk=user_id).first()
def import_events(events, remove_missing_from_source=None):
to_import = []
to_update = []
@ -1608,7 +1429,6 @@ 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)
@ -1635,14 +1455,9 @@ 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.
@ -1670,23 +1485,13 @@ class Event(models.Model):
for e in to_import:
if e.is_event_long_duration():
e.status = Event.STATUS.DRAFT
e.set_message(
Message(subject=_('Import'),
name=_('import process'),
message=_("The duration of the event is a little too long for direct publication. Moderators can choose to publish it or not."),
message_type=Message.TYPE.IMPORT_PROCESS)
)
# then import all the new events
imported = Event.objects.bulk_create(to_import)
# update organisers (m2m relation)
for i, ti in zip(imported, to_import):
if ti.has_pending_organisers() and ti.pending_organisers is not None:
if ti.has_pending_organisers():
i.organisers.set(ti.pending_organisers)
if ti.has_message():
msg = ti.get_message()
msg.related_event = i
msg.save()
nb_updated = Event.objects.bulk_update(
to_update,
@ -1760,12 +1565,13 @@ 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(day, qs = Event.objects.filter(status=Event.STATUS.PUBLISHED)).get_events()
day_events = CalendarDay(self.start_day).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)
]
@ -1775,10 +1581,10 @@ class Event(models.Model):
return (dtstart <= e_dtstart <= dtend) or (e_dtstart <= dtstart <= e_dtend)
def export_to_ics(events, request):
def export_to_ics(events):
cal = icalCal()
# Some properties are required to be compliant
cal.add("prodid", "-//Pommes de lune//pommesdelune.fr//")
cal.add("prodid", "-//My calendar product//example.com//")
cal.add("version", "2.0")
for event in events:
@ -1829,12 +1635,9 @@ 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", description + url
"description", event.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)
@ -1870,50 +1673,16 @@ class Event(models.Model):
return [Event.get_count_modification(w) for w in when_list]
class Message(models.Model):
class TYPE(models.TextChoices):
FROM_CONTRIBUTOR = "from_contributor", _("From contributor")
IMPORT_PROCESS = "import_process", _("Import process")
CONTACT_FORM = "contact_form", _("Contact form")
EVENT_REPORT = "event_report", _("Event report")
FROM_CONTRIBUTOR_NO_MSG = "from_contrib_no_msg", _("From contributor (without message)")
class ContactMessage(models.Model):
class Meta:
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']),
]
verbose_name = _("Contact message")
verbose_name_plural = _("Contact messages")
subject = models.CharField(
verbose_name=_("Subject"),
help_text=_("The subject of your message"),
max_length=512,
)
related_event = models.ForeignKey(
Event,
verbose_name=_("Related event"),
help_text=_("The message is associated with this event."),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
)
user = models.ForeignKey(
User,
verbose_name=_("Author of the message"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
)
name = models.CharField(
verbose_name=_("Name"),
help_text=_("Your name"),
@ -1953,18 +1722,8 @@ class Message(models.Model):
null=True,
)
message_type = models.CharField(
verbose_name=_("Type"),
max_length=20,
choices=TYPE.choices,
default=None, null=True
)
def nb_open_messages():
return Message.objects.filter(Q(closed=False)&Q(spam=False)&Q(message_type__in=[Message.TYPE.CONTACT_FORM, Message.TYPE.EVENT_REPORT, Message.TYPE.FROM_CONTRIBUTOR])).count()
def get_absolute_url(self):
return reverse("message", kwargs={"pk": self.pk})
def nb_open_contactmessages():
return ContactMessage.objects.filter(closed=False).count()
class RecurrentImport(models.Model):
@ -1985,7 +1744,6 @@ 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")
@ -2053,12 +1811,6 @@ 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"),
@ -2119,11 +1871,6 @@ 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,10 +56,9 @@ INSTALLED_APPS = [
"robots",
"debug_toolbar",
"cache_cleaner",
"honeypot",
]
HONEYPOT_FIELD_NAME = "alias_name"
SITE_ID = 1
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
@ -73,7 +72,6 @@ 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",
@ -147,7 +145,7 @@ TIME_ZONE = "Europe/Paris"
USE_I18N = True
USE_TZ = False
USE_TZ = True
LANGUAGES = (
("fr", _("French")),
@ -250,7 +248,6 @@ LOCATION_FIELD = {
# stop robots
ROBOTS_USE_SITEMAP = False
ROBOTS_SITE_BY_REQUEST = 'cached-sitemap'
# debug
if DEBUG:

View File

@ -1,13 +0,0 @@
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.

After

Width:  |  Height:  |  Size: 237 KiB

View File

@ -34,17 +34,11 @@ const openModal = (modal, back=true) => {
}
setTimeout(function() {
visibleModal = modal;
console.log("ici");
const mask = visibleModal.querySelector(".h-mask");
mask.classList.add("visible");
}, 350);
}, 500);
};
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

@ -1,673 +0,0 @@
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,9 +44,6 @@ $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%);
}
@ -150,7 +147,7 @@ details[role="list"] summary + ul li.selected>a:hover {
}
}
.suggestions {
.suggested-tags {
font-size: 80%;
}
}
@ -204,16 +201,10 @@ details[role="list"] summary + ul li.selected>a:hover {
text-align: left;
}
.suggestions .small-cat {
.suggested-tags .small-cat {
overflow: visible;
}
.small-location {
@extend .small-cat;
border-color: var(--contrast);
color: var(--contrast);
}
.circ-cat.circ-large {
height: 2.6em;
width: 2.6em;
@ -298,7 +289,15 @@ svg {
width: 100%;
padding: 0.3em;
margin: 0 0 0.5em 0;
float: right;
}
@media only screen and (min-width: 550px) {
.illustration {
width: 40%;
float: right;
margin: 0 0 0.5em .5em;
}
}
@media only screen and (min-width: 992px) {
@ -324,7 +323,6 @@ footer [data-tooltip] {
scroll-behavior: smooth;
transition-duration: 200ms;
.cat {
margin-right: 0;
}
@ -497,15 +495,6 @@ body > main {
padding-top: 0.2em;
}
body.authenticated > main {
padding-top: 0.8em;
}
@media only screen and (min-width: 700px) {
body.authenticated > main {
padding-top: 0.2em;
}
}
article {
margin: 1em 0;
}
@ -614,7 +603,7 @@ header .remarque {
}
.form.recent, .form.main-filter, .search .form {
#id_status>div, #id_representative>div {
#id_status>div {
display: inline-block;
margin-right: 2em;
}
@ -862,7 +851,7 @@ nav>div {
}
@media only screen and (min-width: 1400px) {
@media only screen and (min-width: 992px) {
.header li {
float: left;
}
@ -904,39 +893,6 @@ nav>div {
color: var(--secondary-inverse);
}
#badges {
position: absolute;
font-size: 70%;
top:3.5em;
left: 0;
padding: 0.2em .5em 0.2em 0.2em;
background: var(--card-sectionning-background-color);
display: inline-block;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
.badge {
margin: 0;
border-radius: 0;
}
.link {
margin-left: .5em;
}
}
@media only screen and (min-width: 700px) {
#badges {
border-radius: 0 0 var(--border-radius) var(--border-radius);
left: 50%;
top: 0;
transform: translate(-50%, 0);
padding: 0 .5em .2em .5em;
z-index: 1000;
}
}
.tw-badge {
background: black;
border-color: black;
@ -1444,28 +1400,17 @@ img.preview {
top: 0;
z-index: 10;
}
@media only screen and (min-width: 600px) {
.single-event, .tag-description {
display: grid;
grid-template-columns: 60% auto;
grid-column-gap: 1em;
}
header {
grid-column: 1 / 3;
}
}
@media only screen and (min-width: 992px) {
.resume {
column-count: 4;
}
.single-event, .tag-description {
display: grid;
grid-template-columns: 30% auto 14em;
grid-column-gap: 1em;
header {
margin: 0;
grid-column: 1 / 2;
}
.illustration {
width: auto;
@ -1483,38 +1428,19 @@ img.preview {
}
}
.header-complement {
float: none;
}
@media only screen and (min-width: 992px) {
.header-complement {
float: left;
clear: both;
}
}
form.messages {
div {
width: 100%;
display: block;
fieldset div {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin-right: 1em;
}
}
@media only screen and (min-width: 800px) {
form.messages div, form.moderation-events {
@media only screen and (min-width: 992px) {
display: grid;
grid-template-columns: repeat(3, 1fr);
:last-child {
grid-column: 1 / 4;
}
div fieldset div {
display: block;
}
grid-template-columns: repeat(2, 50%);
}
fieldset {
float: left;
margin-right: 1em;
}
label {
clear: both;
float: left;
}
}
.moderate-preview .event-body {
@ -1522,11 +1448,6 @@ form.messages {
overflow-y: auto;
}
#moderate-form #id_status {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
label.required::after {
content: ' *';
color: red;
@ -1572,234 +1493,6 @@ label.required::after {
}
}
.maskable_group {
margin: 0.5em 0;
.body_group.closed {
display: none;
}
.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,53 +17,13 @@
{% 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>
</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>
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'moderate' %}" role="button">Modérer {% picto_from_name "check-square" %}</a>
</div>
<h2>Activité des derniers jours</h2>
</header>
<h3>Résumé des activités</h3>
@ -76,8 +36,7 @@
{% include "agenda_culturel/rimports-info-inc.html" with all=1 %}</p>
</article>
</div>
<article>
<header>
<div class="slide-buttons">

View File

@ -1,27 +0,0 @@
{% 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

@ -0,0 +1,37 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% block title %}{% block og_title %}Contact{% endblock %}{% endblock %}
{% block entete_header %}
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
<h1>Contact</h1>
<article>
{% url 'contact' as local_url %}
{% include "agenda_culturel/static_content.html" with name="contact" url_path=local_url %}
</article>
<article>
<header>
<p class="message warning"><strong>Attention&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>
</header>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Envoyer">
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% load utils_extra %}
{% block title %}{% block og_title %}Message de contact : {{ obj.subject }}{% endblock %}{% endblock %}
{% block entete_header %}
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
{% endblock %}
{% block sidemenu-bouton %}
<li><a href="#contenu-principal" aria-label="Aller au contenu">{% picto_from_name "chevron-up" %}</a></li>
<li><a href="#sidebar" aria-label="Aller au menu latéral">{% picto_from_name "chevron-down" %}</a></li>
{% endblock %}
{% block content %}
<div class="grid two-columns">
<div>
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'delete_contactmessage' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
<h1>Modération du message «&nbsp;{{ object.subject }}&nbsp;»</h1>
<ul>
<li>Date&nbsp;: {{ object.date }}</li>
<li>Auteur&nbsp;: {{ object.name }} <a href="mailto:{{ object.email }}">{{ object.email }}</a></li>
</ul>
</header>
<div>
{{ object.message }}
</div>
</article>
<article>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Enregistrer">
</form>
</article>
</div>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
</div>
{% endblock %}

View File

@ -34,10 +34,8 @@
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Sujet</th>
<th>Auteur</th>
<th>Événement</th>
<th>Fermé</th>
<th>Spam</th>
</tr>
@ -46,10 +44,8 @@
{% for obj in paginator_filter %}
<tr>
<td>{{ obj.date }}</td>
<td>{% if obj.message_type %}{{ obj.get_message_type_display }}{% else %}-{% endif %}</td>
<td><a href="{% url 'message' obj.pk %}">{{ obj.subject }}</a></td>
<td>{% if obj.user %}<em>{{ obj.user }}</em>{% else %}{% if obj.name %}{{ obj.name }}{% else %}-{% endif %}{% endif %}</td>
<td>{% if obj.related_event %}<a href="{{ obj.related_event.get_absolute_url }}">{{ obj.related_event.pk }}</a>{% else %}-{% endif %}</td>
<td><a href="{% url 'contactmessage' obj.pk %}">{{ obj.subject }}</a></td>
<td>{{ obj.name }}</td>
<td>{% if obj.closed %}{% picto_from_name "check-square" "fermé" %}{% else %}{% picto_from_name "square" "ouvert" %}{% endif %}</td>
<td>{% if obj.spam %}{% picto_from_name "check-square" "spam" %}{% else %}{% picto_from_name "square" "non spam" %}{% endif %}</td>
</tr>
@ -61,7 +57,7 @@
</footer>
</article>
{% include "agenda_culturel/side-nav.html" with current="messages" %}
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
</div>
{% 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">modifier en copie locale {% picto_from_name "plus-circle" %}</a>
<a href="{% url 'clone_edit' event.id %}" role="button">créer une copie locale {% picto_from_name "plus-circle" %}</a>
{% endif %}
{% endwith %}
{% else %}

View File

@ -1,9 +0,0 @@
Bonjour,
Nous avons le plaisir de t'informer que l'événement « {{ event_title }} » que tu as proposé sur {{ sitename }} a été validé et publié par l'équipe de modération.
Tu peux dès maintenant le retrouver à l'adresse suivante :
- {{ url }}
Merci de participer à l'amélioration de {{ sitename }}. N'hésites pas à continuer à contribuer en ajoutant de nouveaux événements, ça nous fait bien plaisir.
L'équipe de modération.

View File

@ -1,8 +0,0 @@
Bonjour,
Nous avons la dure tâche de t'informer que l'événement « {{ event_title }} » que tu avais proposé sur {{ sitename }} n'a pas été retenu par l'équipe de modération.
Nous te remercions pour cette proposition, et espérons qu'une prochaine fois, ta proposition correspondra à la ligne portée par {{ sitename }}.
L'équipe de modération.

View File

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

View File

@ -1,12 +1,10 @@
<footer class="remarque">
<strong>Informations complémentaires non éditables</strong>
Informations complémentaires non éditables&nbsp;:
<ul>
{% 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.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 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{% if form.is_clone_from_url %} et modérer{% endif %}">
<input type="submit" value="Enregistrer">
</div>
</form>

View File

@ -33,39 +33,31 @@
</p>
</header>
<form method="post" enctype="multipart/form-data" id="moderate-form">{% csrf_token %}
<form method="post" enctype="multipart/form-data">{% csrf_token %}
<div class="grid moderate-preview">
<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>
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event noedit=1 %}
<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é {% if event.moderation_by_user %}par {<em>{ event.moderation_by_user.username }}</em> {% endif %}le {{ event.moderated_date }}.
<p class="message info">Cet événement a déjà été modéré par le {{ event.moderated_date }}.
Vous pouvez bien sûr modifier de nouveau ces méta-informations en utilisant
le formulaire ci-après.
</p>
@ -77,13 +69,23 @@
</div>
<div class="grid buttons">
{% if pred %}
<a href="{% url 'moderate_event' pred %}" class="secondary" role="button">&lt; Revenir au précédent</a>
<a href="{% url 'moderate_event' pred %}" role="button">🠄 Revenir au précédent</a>
{% else %}
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
{% endif %}
<input type="submit" value="Enregistrer" name="save">
<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>
{% 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">
</div>
</form>
</article>

View File

@ -2,7 +2,6 @@
{% load tag_extra %}
{% load utils_extra %}
{% load static %}
{% load locations_extra %}
{% if noarticle == 0 %}
<article id="filters">
@ -66,10 +65,8 @@
<button type="submit">Appliquer le filtre</button>
</form>
</details>
<div class="suggestions">
Suggestion&nbsp;:
{% show_suggested_positions filter=filter %}
{% show_suggested_tags filter=filter %}
<div class="suggested-tags">
{% show_suggested_tags filter=filter %}
</div>
</div>
<div class="clear"></div>

View File

@ -6,13 +6,7 @@
{% for group, fields in form.fields_by_group %}
<div {% if group.maskable %}class="maskable_group"{% endif %} id="group_{{ group.id }}">
{% if group.maskable %}
<input
class="toggle_body"
type="checkbox"
id="maskable_group_{{ group.id }}"
name="group_{{ group.id }}"
{% if not group.default_masked %}checked{% endif %}
><label for="maskable_group_{{ group.id }}">{{ group.label }}</label>
<input class="toggle_body" type="checkbox" id="maskable_group_{{ group.id }}" name="group_{{ group.id }}"><label for="maskable_group_{{ group.id }}">{{ group.label }}</label>
{% endif %}
<div class="error_group">
{% for field in fields %}
@ -42,9 +36,7 @@
<script>
const maskables = document.querySelectorAll('.maskable_group');
maskables.forEach(function (item) {
if (!item.checked) {
item.querySelector('.body_group').classList.add('closed');
}
item.querySelector('.body_group').classList.add('closed');
console.log('item ' + item);
item.querySelector('.toggle_body').addEventListener('change', (event) => {

View File

@ -21,7 +21,7 @@
<form method="post" action="">
{% csrf_token %}
{{ form.media }}
{{ form }}
{{ form.as_p }}
<input type="submit" value="Lancer l'import" id="import-button">
</form>
<p>Si tu as plein d'événements à ajouter, tu peux les <a href="{% url 'add_event_urls' %}" >ajouter par lots</a>.</p>

View File

@ -28,12 +28,6 @@
{{ formset.management_form }}
{% csrf_token %}
{% if contactform %}
<article>
{{ contactform }}
</article>
{% endif %}
{% for form in formset %}
<article>
<header>

View File

@ -1,51 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% load honeypot %}
{% block title %}{% block og_title %}{% if form.event %}Contact au sujet de l'événement {{ form.event.title }}{% else %}
Contact{% endif %}{% endblock %}{% endblock %}
{% block entete_header %}
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
<article>
<header>
<h1>{% if form.event %}Contact au sujet de l'événement «&nbsp;{{ form.event.title }}&nbsp;»{% else %}
Contact{% endif %}</h1>
{% if not form.event %}
<article>
{% url 'contact' as local_url %}
{% include "agenda_culturel/static_content.html" with name="contact" url_path=local_url %}
</article>
{% endif %}
<p class="message warning"><strong>Attention&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
afin que l'on puisse répondre à tes demandes ou remarques.
</p>
{% endif %}
</header>
<form method="post">{% csrf_token %}
{% render_honeypot_field "alias_name" %}
{{ form.media }}
{{ form.as_p }}
<input type="submit" value="Envoyer">
</form>
</article>
{% endblock %}

View File

@ -1,63 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% load utils_extra %}
{% block title %}{% block og_title %}Message {{ obj.subject }}{% endblock %}{% endblock %}
{% block entete_header %}
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
{% endblock %}
{% block sidemenu-bouton %}
<li><a href="#contenu-principal" aria-label="Aller au contenu">{% picto_from_name "chevron-up" %}</a></li>
<li><a href="#sidebar" aria-label="Aller au menu latéral">{% picto_from_name "chevron-down" %}</a></li>
{% endblock %}
{% block content %}
<div class="grid two-columns">
<div>
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'delete_message' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
<h1>Message «&nbsp;{{ object.subject }}&nbsp;»</h1>
<ul>
<li>Date&nbsp;: {{ object.date.date }} à {{ object.date.time }}</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 %}
<li>Type&nbsp;: {% if object.message_type %}{{ object.get_message_type_display }}{% else %}-{% endif %}</li>
</ul>
</header>
<div>
{{ object.message | safe }}
</div>
</article>
<article>
{% if object.message_type == "from_contributor" or object.message_type == "from_contrib_no_msg" %}<p class="message info">Ce message a été envoyé par une personne lors
de l'ajout d'un événement.
{% if object.closed %}
En décochant fermé, vous modifiez manuellement son statut, et cela pourra entraîner l'envoi d'un message de notification
lors de la modification future de l'événement associé.{% else %}
En cochant fermé, vous modifiez manuellement son statut, et cela empêchera l'envoi d'un message de notification
lors de la modification future de l'événement associé.
{% endif %}
</p>{% endif %}
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Enregistrer">
</form>
</article>
</div>
{% include "agenda_culturel/side-nav.html" with current="messages" %}
</div>
{% endblock %}

View File

@ -35,8 +35,12 @@
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,11 +3,10 @@
{% 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_uri:request }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block og_image %}{% if event.has_image_url %}{{ event.get_image_url }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block og_description %}{% if event.description %}{{ event.description |truncatewords:20|linebreaks }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block entete_header %}
@ -17,62 +16,21 @@
{% 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 id="chronology">
<article>
<header>
<h2>Chronologie</h2>
<h2>Informations internes</h2>
</header>
{% for step in event.chronology %}
{% if step.is_date %}
<div class="entree dateline">
<div><span class="ts">{{ step.timestamp }}</span></div>
<div>
{% if step.data == "created_date" %}<em>création</em>{% if event.created_by_user %} par {{ event.created_by_user.username }}{% endif %}{% endif %}
{% if step.data == "modified_date" %}<em>dernière modification</em>{% if event.modified_by_user %} par {{ event.modified_by_user.username }}{% endif %}{% endif %}
{% if step.data == "moderated_date" %}<em>dernière modération</em>{% if event.moderated_by_user %} par {{ event.moderated_by_user.username }}{% endif %}{% endif %}
{% if step.data == "imported_date" %}<em>dernière importation</em>{% if event.imported_by_user %} par {{ event.imported_by_user.username }}{% endif %}{% endif %}
</div>
</div>
{% else %}
<div class="entree">
<div><span class="ts">{{ step.timestamp }}</span></div>
<div>
<header><strong>Message{% if not step.data.closed %} (ouvert){% endif %}</strong>{% if step.data.related_event and event != step.data.related_event %} sur
<a href="{{ step.data.related_event.get_absolute_url }}">une autre</a> version{% endif %}&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 {% if step.data.name %}{{ step.data.name }}{% if step.data.email %} (<a href="mailto: {{ step.data.email }}">{{ step.data.email }}</a>){% endif %}{% else %} <a href="mailto: {{ step.data.email }}">{{ step.data.email }}</a>{% endif %}{% endif %}</header>
<div class="texte">{{ step.data.message|safe }}</div>
{% if step.data.comments %}
<div><strong>Commentaire&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 %}
{% include "agenda_culturel/event-info-inc.html" with object=event %}
</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 %}
@ -93,7 +51,39 @@
</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 %}
@ -132,15 +122,12 @@
{% else %}
Signaler comme doublon
{% endif %}</a>
<a role="button" href="{% url 'message_for_event' event.pk %}">Signaler cet événement</a>
</article>
</article>
</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 }}" class="visible-link">{{ day.date | date:"l j" }}</a></h3}>
<h3><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h3}>
</header>
{% if day.events %}
<ul>
@ -107,7 +107,7 @@
{% if event.start_day == day.date and event.start_time %}
{{ event.start_time }}
{% endif %}
<a href="{{ event.get_absolute_url }}">{{ event|picto_status }} {{ event.title|no_emoji }} {{ event|tw_badge }}</a>
<a href="{{ event.get_absolute_url }}">{{ event|picto_status }} {{ event.title }} {{ event|tw_badge }}</a>
</li>
{% endfor %}
</ul>
@ -121,8 +121,7 @@
</article>
</dialog>
{% endfor %}
<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>
<ul>
{% endif %}
</ul>
</article>

View File

@ -39,13 +39,13 @@
<li><strong>Adresse naviguable&nbsp;:</strong> <a href="{{ object.browsable_url }}">{{ object.browsable_url }}</a></li>
<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{% if object.forceLocation %} (forcée){% endif %}&nbsp;:</strong> {{ object.defaultLocation }}</li>{% endif %}
<li><strong>Publié&nbsp;:</strong> {{ object.defaultPublished }}</li>
<li><strong>Localisation&nbsp;:</strong> {{ object.defaultLocation }}</li>
<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>Organisateur&nbsp;:</strong> <a href="{{ object.defaultOrganiser.get_absolute_url }}">{{ object.defaultOrganiser }}</a></li>
<li><strong>Étiquettes&nbsp;:</strong>
{% for tag in object.defaultTags %}
<a href="{% url 'view_tag' tag %}">{{ tag|tw_highlight }}</a>{% if not forloop.last %}, {% endif %}
{{ tag|tw_highlight }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</li>
</ul>

View File

@ -95,19 +95,13 @@
<h3>{{ ti.short_name }} <a class="badge simple" href="#{{ ti.id }}" data-tooltip="Aller à {{ ti.name }}">{{ ti.events|length }} {% picto_from_name "chevrons-down" %}</a></h3>
<ul>
{% for event in ti.events %}
{% if event.is_first_after_now %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
<li>{{ event.category | circle_cat:event.has_recurrences }}
{% if event.start_time %}
{{ event.start_time }}
{% endif %}
{{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title|no_emoji }}</a> {{ event|tw_badge }}
{{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title }}</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

@ -14,7 +14,7 @@
<script src="{% static 'js/calendar-buttons.js' %}"></script>
{% endblock %}
{% block title %}{% block og_title %}{% if calendar.today_in_calendar %}Sorties culturelles cette semaine à Clermont-Ferrand et aux environs{% else %}Semaine du {{ calendar.firstdate|date|frdate }}{% endif %}{% endblock %}{% endblock %}
{% block title %}{% block og_title %}Semaine du {{ calendar.firstdate|date|frdate }}{% endblock %}{% endblock %}
{% block ce_mois_ci_parameters %}{% block cette_semaine_parameters %}{% block a_venir_parameters %}?{{ filter.get_url }}{% endblock %}{% endblock %}{% endblock %}
@ -37,7 +37,7 @@
<div>
{% if calendar.firstdate|shift_day:-1|not_before_first %}
{% if calendar.lastdate|not_after_last %}
<a role="button" href="{% url 'week_view' calendar.previous_week|weekyear calendar.previous_week|week %}?{{ filter.get_url }}">
<a role="button" href="{% url 'week_view' calendar.previous_week.year 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|weekyear calendar.next_week|week %}?{{ filter.get_url }}">suivante
<a role="button" href="{% url 'week_view' calendar.next_week.year 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|weekyear 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.year calendar.previous_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-left" %}</a></div>
{% endif %}
{% endif %}
@ -80,28 +80,20 @@
</script>
{% endif %}
<header{% if day.is_today %} id="today"{% endif %}>
<h2><a class="visible-link" href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h2>
<h2><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h2>
</header>
{% if day.events %}
<ul>
{% for event in day.events %}
{% if event.is_first_after_now %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
<li>{{ event.category | circle_cat:event.has_recurrences }}
{% if event.start_day == day.date and event.start_time %}
{{ event.start_time }}
{% endif %}
{{ event|picto_status }} <a href="{{ event.get_absolute_url }}" data-target="event-{{ event.id }}" onClick="toggleModal(event)">{{ event.title|no_emoji }}</a>
{{ event|picto_status }} <a href="{{ event.get_absolute_url }}" data-target="event-{{ event.id }}" onClick="toggleModal(event)">{{ event.title }}</a>
{{ event|tw_badge }}
<dialog id="event-{{ event.id }}">
<article>
{% 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">
<header>
<a href="#event-{{ event.id }}"
aria-label="Fermer"
class="close"
@ -133,7 +125,6 @@
{% 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>
@ -156,10 +147,6 @@
</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>
@ -173,7 +160,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|weekyear 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.year calendar.next_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-right" %}</a></div>
{% endif %}
{% endif %}

View File

@ -1,23 +1,15 @@
<!DOCTYPE html>
<html lang="fr">
{% load event_extra %}
{% load cache %}
{% load messages_extra %}
{% load utils_extra %}
{% load duplicated_extra %}
{% load rimports_extra %}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pommes de lune — {% block title %}{% endblock %}</title>
<meta name="google-site-verification" content="pvRD0rc_xIE-1IYmbao0kj5ngGo1IWxJqKwoxrQwxuA" />
<meta name="keywords" content="Clermont-Ferrand, Puy-de-Dôme, agenda culturel, agenda participatif, sortir à clermont, sorties, concerts, théâtre, danse, animations, ateliers, lectures">
{% load static %}
<meta property="og:title" content="Pommes de lune — {% block og_title %}{% endblock %}" />
<meta property="og:description" content="{% block og_description %}Où sortir à Clermont-Ferrand? Retrouve tous les bons plans sur l'agenda participatif des événements culturels à Clermont-Ferrand et dans le Puy-de-Dôme{% endblock %}" />
<meta property="og:image" content="{% block og_image %}https://{{ request.get_host }}{% get_media_prefix %}screenshot.png{% endblock %}" />
<meta property="og:locale" content="fr_FR" />
<meta property="og: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:url" content="{{ request.build_absolute_uri }}" />
{% if debug %}
@ -35,8 +27,13 @@
{% block entete_header %}
{% endblock %}
</head>
<body class="{% block body-class %}contenu{% endblock %} {% if user.is_authenticated %}authenticated{% endif %}">
{% 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>
{% block sidemenu-bouton %}{% endblock %}
@ -50,9 +47,10 @@
<input class="menu-btn" type="checkbox" id="menu-btn" />
<label class="menu-icon" for="menu-btn">{% picto_from_name "menu" %}</label>
<ul class="menu">
{% if user.is_authenticated %}{% block configurer-menu %}<li id="menu-configurer" class="configurer-bouton"><a href="{% url 'administration' %}">Administrer {% picto_from_name "settings" %}</a></li>{% endblock %}{% endif %}
{% block ajouter-menu %}<li id="menu-ajouter" class="ajouter-bouton"><a href="{% url 'add_event' %}">Ajouter un événement {% picto_from_name "plus-circle" %}</a></li>{% endblock %}
{% block rechercher-menu %}<li id="menu-rechercher" class="rechercher-bouton"><a href="{% url 'event_search' %}">Rechercher {% picto_from_name "search" %}</a></li>{% endblock %}
<li><a href="{% url 'a_venir' %}{% block a_venir_parameters %}{% endblock %}">Maintenant</a></li>
<li><a href="{% url 'a_venir' %}{% block a_venir_parameters %}{% endblock %}">À venir</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>
@ -66,35 +64,30 @@
</li>
<li>
<div>
{% if perms.agenda_culturel.view_recurrentimport %}
{% show_badges_rimports "bottom" %}
{% endif %}
{% if perms.agenda_culturel.change_event %}
{% show_badges_events "bottom" %}
{% endif %}
{% if perms.agenda_culturel.change_duplicatedevents %}
{% show_badge_duplicated "bottom" %}
{% endif %}
{% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %}
{% show_badge_unknown_places "bottom" %}
{% endif %}
{% if perms.agenda_culturel.view_contactmessage %}
{% show_badge_contactmessages "bottom" %}
{% endif %}
{% if user.is_authenticated %}
{{ user.username }} @
{% endif %}
<a href="{% url 'home' %}" aria-label="Retour accueil">Pommes de lune</a>
</div>
<div class="soustitre">Agenda participatif des sorties culturelles à Clermont-Ferrand et aux environs</div>
<div class="soustitre">Événements culturels à Clermont-Ferrand et aux environs</div>
</li>
</ul>
</nav>
{% if user.is_authenticated %}
<div id="badges">
{% if perms.agenda_culturel.view_recurrentimport %}
{% show_badges_rimports "bottom" %}
{% endif %}
{% if perms.agenda_culturel.change_event %}
{% show_badges_events "bottom" %}
{% endif %}
{% if perms.agenda_culturel.change_duplicatedevents %}
{% show_badge_duplicated "bottom" %}
{% endif %}
{% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %}
{% show_badge_unknown_places "bottom" %}
{% endif %}
{% if perms.agenda_culturel.view_message %}
{% show_badge_messages "bottom" %}
{% endif %}
<a class="link" href="{% url 'administration' %}">Administrer {% picto_from_name "settings" %}</a>
</div>
{% endif %}
</div>
<main class="container{% block fluid %}-fluid{% endblock %}">
{% if messages %}

View File

@ -22,26 +22,9 @@
<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 %}
<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>
{{ form.as_grid }}
<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

@ -6,8 +6,6 @@
{% block configurer-bouton %}{% endblock %}
{% block entete_header %}
<script src="{% static 'choicejs/choices.min.js' %}"></script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
@ -28,35 +26,4 @@
</form>
</article>
<script>
show_firstgroup = {
choice(classes, choice) {
const i = Choices.defaults.templates.choice.call(this, classes, choice);
if (this.first_group !== null && choice.groupId == this.first_group)
i.classList.add("visible");
return i;
},
choiceGroup(classes, group) {
const g = Choices.defaults.templates.choiceGroup.call(this, classes, group);
if (this.first_group === undefined && group.value == "Suggestions")
this.first_group = group.id;
if (this.first_group !== null && group.id == this.first_group)
g.classList.add("visible");
return g;
}
};
const tags = document.querySelector('#id_defaultTags');
const choices_tags = new Choices(tags,
{
placeholderValue: 'Sélectionner les étiquettes par défaut',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
}
);
</script>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% load event_extra %}
{% load messages_extra %}
{% load contactmessages_extra %}
{% load duplicated_extra %}
{% load utils_extra %}
<aside id="sidebar">
@ -56,11 +56,11 @@
</ul>
</nav>
{% endif %}
{% if perms.agenda_culturel.view_message %}
{% if perms.agenda_culturel.view_contactmessage %}
<h3>Messages</h3>
<nav>
<ul>
<li><a {% if current == "messages" %}class="selected" {% endif %}href="{% url 'messages' %}">Messages de contact</a>{% show_badge_messages "left" %}</li>
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
</ul>
</nav>
{% endif %}
@ -68,7 +68,6 @@
<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,11 +6,7 @@
<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 %}
<div class="small-ephemeride">
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
</div>
{% else %}
{% if day != 0 %}
{% if event|can_show_start_time:day %}
{% if event.start_time %}
<article class='ephemeris-hour'>
@ -26,7 +22,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 %}
@ -54,7 +50,11 @@
</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,13 +86,12 @@
</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,17 +22,16 @@
{% 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 %}
<p>{% picto_from_name "map-pin" %}
<a href="{{ event.exact_location.get_absolute_url }}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a></p>
<a href="{{ event.exact_location.get_absolute_url }}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a>
{% else %}
{% if perms.agenda_culturel.change_event and perms.agenda_culturel.change_place %}
<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>
<a href="{% url 'add_place_to_event' event.pk %}" class="missing-data">{{ event.location }}</a>
{% else %}
{% if event.location %}<p>{% picto_from_name "map-pin" %} {{ event.location }}</p>{% endif %}
{{ event.location }}
{% endif %}
{% endif %}
</p>
@ -76,15 +75,6 @@
</p>
{% endif %}
{% endif %}
{% if perms.agenda_culturel.change_message %}
{% if event.message_set.all.count > 0 %}
<p class="remarque">Cet événement a été l'objet {% if event.message_set.all.count == 1 %}d'un message{% else %}de messages{% endif %}
{% for cm in event.message_set.all %}
<a href="{{ cm.get_absolute_url }}">le {{ cm.date.date }} à {{ cm.date.time }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
</header>
<div class="event-body">
@ -134,24 +124,9 @@
{% include "agenda_culturel/event-date-info-inc.html" %}
</div>
<div class="buttons">
{% 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 %}
<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 %}
</div>
</footer>

View File

@ -62,12 +62,6 @@
{% endif %}
<footer>
{% if user.is_authenticated and rimports %}
<p>Cette étiquette est ajoutée par défaut {% if rimports.count == 1 %}à l'import récurrent{% else %}aux imports récurrents&nbsp;:{% endif %}
{% for ri in rimports %}
<a href="{{ ri.get_absolute_url }}">{{ ri.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}</p>
{% endif %}
{% include 'agenda_culturel/paginator.html' %}
</footer>
</article>

View File

@ -20,12 +20,6 @@
{% else %} sera bien sûr conservé, mais perdra cette étiquette.
{% endif %}</p>
{% endif %}
{% if nbi > 0 %}
<p>Remarquez qu'elle est associée à {{ nbi }} import{{ nbs|pluralize }} récurrent{{ nbs|pluralize }}, qui
{% if nbi > 1 %} seront bien sûr conservés, mais perdront cette étiquette.
{% else %} sera bien sûr conservé, mais perdra cette étiquette.
{% endif %}</p>
{% endif %}
{% if obj %}
<p>Différentes informations sont associées à cette étiquette (description, suggestion d'inclusion, etc)
seront également perdues lors de cette suppression.</p>

View File

@ -17,7 +17,7 @@
<article>
<header>
<h1>Renommer l'étiquette {{ tag }}</em></h1>
<p>En renommant l'étiquette {{ tag }}, vous remplacerez cette étiquette par la nouvelle pour tous les événements et tous les imports récurrents concernés.</p>
<p>En renommant l'étiquette {{ tag }}, vous remplacerez cette étiquette par la nouvelle pour tous les événements concernés.</p>
</header>

View File

@ -86,6 +86,12 @@ 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

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

View File

@ -179,8 +179,4 @@ 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 ""
@register.filter
def get_image_uri(event, request):
return event.get_image_url(request)
return ""

View File

@ -1,25 +0,0 @@
from django import template
from django.utils.safestring import mark_safe
from django.core.cache import cache
from .utils_extra import picto_from_name
from agenda_culturel.models import ReferenceLocation
register = template.Library()
@register.simple_tag
def show_suggested_positions(filter):
filter.form.full_clean()
if filter.is_filtered_by_position_radius():
return ""
locations = ReferenceLocation.objects.all().filter(suggested_distance__isnull=False).order_by("main", "name")
result = ''
for l in locations:
result += ' <a class="small-location" role="button" href="' + filter.get_url_add_suggested_position(l) + '">' + picto_from_name("map-pin") + ' ' + l.name + ' ' + str(l.suggested_distance) + 'km</a>'
return mark_safe(result)

View File

@ -54,7 +54,7 @@ def tag_not_in_db(tag, tags):
def show_suggested_tags(filter):
filter.form.full_clean()
tags = Tag.objects.all().filter(principal=True).order_by("name")
result = ""
result = "Suggestion&nbsp;:"
for t in tags:
if filter.tag_exists(t.name) and not filter.is_selected_tag(t.name):

View File

@ -9,7 +9,6 @@ from django.urls import reverse_lazy
from django.templatetags.static import static
from string import ascii_uppercase as auc
from django.utils.html import strip_tags
import emoji
register = template.Library()
@ -29,9 +28,6 @@ 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):
@ -141,6 +137,3 @@ def is_string(val):
def html_vide(val):
return len(strip_tags(val).replace("&nbsp;", "").strip()) == 0
@register.filter
def no_emoji(text):
return emoji.replace_emoji(text, replace='')

View File

@ -4,32 +4,10 @@ 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"),
@ -57,13 +35,11 @@ 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/after/<int:pred>", EventModerateView.as_view(), name="moderate_event_step"),
path("event/<int:pk>/moderate-next", moderate_event_next, name="moderate_event_next"),
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("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", 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>",
@ -98,18 +74,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", MessageCreateView.as_view(), name="contact"),
path("messages", view_messages, name="messages"),
path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
path("contact", ContactMessageCreateView.as_view(), name="contact"),
path("contactmessages", contactmessages, name="contactmessages"),
path("contactmessages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
path(
"message/<int:pk>",
MessageUpdateView.as_view(),
name="message",
"contactmessage/<int:pk>",
ContactMessageUpdateView.as_view(),
name="contactmessage",
),
path(
"message/<int:pk>/delete",
MessageDeleteView.as_view(),
name="delete_message",
"contactmessage/<int:pk>/delete",
ContactMessageDeleteView.as_view(),
name="delete_contactmessage",
),
path("imports/", imports, name="imports"),
path("imports/add", add_import, name="add_import"),
@ -157,8 +133,7 @@ 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_shortname"),
path("organisme/<int:pk>-<extra>", OrganisationDetailView.as_view(), name="view_organisation"),
path("organisme/<int:pk>", 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"),
@ -202,14 +177,6 @@ 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="cached-sitemap",
),
path("cache/clear", clear_cache, name="clear_cache"),
]
if settings.DEBUG:

View File

@ -1,114 +0,0 @@
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, ModelFormMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import (
LoginRequiredMixin,
UserPassesTestMixin,
@ -10,12 +10,6 @@ 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
@ -25,9 +19,11 @@ from django.urls import reverse
from collections import Counter
import emoji
from django.forms import formset_factory
from .forms import (
URLSubmissionForm,
EventForm,
EventFormWithContact,
BatchImportationForm,
FixDuplicates,
SelectEventInList,
@ -40,18 +36,13 @@ from .forms import (
MultipleHiddenInput,
EventModerateForm,
TagForm,
TagRenameForm,
MessageForm,
MessageEventForm,
URLSubmissionFormWithContact,
URLSubmissionFormSet,
SimpleContactForm,
TagRenameForm
)
from .filters import (
EventFilter,
EventFilterAdmin,
MessagesFilterAdmin,
ContactMessagesFilterAdmin,
SimpleSearchEventFilter,
SearchEventFilter,
DuplicatedEventsFilter,
@ -63,7 +54,7 @@ from .models import (
Category,
Tag,
StaticContent,
Message,
ContactMessage,
BatchImportation,
DuplicatedEvents,
RecurrentImport,
@ -77,7 +68,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, BooleanField, ExpressionWrapper
from django.db.models import Q, Subquery, OuterRef, Count, F, Func
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -226,7 +217,7 @@ def month_view(request, year=None, month=None):
def week_view(request, year=None, week=None, home=False):
now = date.today()
if year is None:
year = now.isocalendar()[0]
year = now.year
if week is None:
week = now.isocalendar()[1]
@ -301,7 +292,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, None, True, user_id=request.user.pk if request.user else None)
import_events_from_url.delay(url, None, True)
messages.success(request, _("The event update has been queued and will be completed shortly."))
return HttpResponseRedirect(event.get_absolute_url())
@ -312,6 +303,7 @@ class EventUpdateView(
model = Event
permission_required = "agenda_culturel.change_event"
form_class = EventForm
success_message = _("The event has been successfully modified.")
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@ -320,16 +312,6 @@ class EventUpdateView(
kwargs["is_simple_cloning"] = self.is_simple_cloning
return kwargs
def get_success_message(self, cleaned_data):
txt = _(" A message has been sent to the person who proposed the event.") if hasattr(self, "with_msg") else ""
return mark_safe(_('The event has been successfully modified.') + txt)
def form_valid(self, form):
form.instance.set_processing_user(self.request.user)
self.with_message = form.instance.notify_if_required(self.request)
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('/')
@ -345,7 +327,6 @@ 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
@ -367,28 +348,21 @@ class EventModerateView(
permission_required = "agenda_culturel.change_event"
template_name = "agenda_culturel/event_form_moderate.html"
form_class = EventModerateForm
def get_success_message(self, cleaned_data):
txt = _(" A message has been sent to the person who proposed the event.") if hasattr(self, "with_msg") else ""
return mark_safe(_('The event <a href="{}">{}</a> has been moderated with success.').format(self.object.get_absolute_url(), self.object.title) + txt)
success_message = _("The event has been successfully moderated.")
def is_moderate_next(self):
return "after" in self.request.path.split('/')
return "moderate-next" 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(start_day, start_time):
def get_next_event(self, 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__gt=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))))
else:
qs = qs.filter(start_day__gte=start_day)
@ -409,15 +383,19 @@ class EventModerateView(
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.is_moderate_next():
context['pred'] = self.kwargs["pred"]
context['pred'] = self.kwargs["pk"]
return context
def get_object(self, queryset=None):
if self.is_starting_moderation():
now = datetime.now()
return EventModerateView.get_next_event(now.date(), now.time())
return self.get_next_event(now.date(), now.time())
else:
return super().get_object(queryset)
result = super().get_object(queryset)
if self.is_moderate_next():
return self.get_next_event(result.start_day, result.start_time)
else:
return result
def post(self, request, *args, **kwargs):
try:
@ -429,13 +407,15 @@ 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)
self.with_msg = form.instance.notify_if_required(self.request)
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:
@ -454,30 +434,6 @@ 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(
@ -489,12 +445,9 @@ class EventDeleteView(
success_message = _("The event has been successfully deleted.")
class EventDetailView(UserPassesTestMixin, DetailView, ModelFormMixin):
class EventDetailView(UserPassesTestMixin, DetailView):
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 (
@ -504,7 +457,6 @@ class EventDetailView(UserPassesTestMixin, DetailView, ModelFormMixin):
def get_object(self):
o = super().get_object()
o.download_missing_image()
y = self.kwargs["year"]
m = self.kwargs["month"]
d = self.kwargs["day"]
@ -512,30 +464,6 @@ class EventDetailView(UserPassesTestMixin, DetailView, ModelFormMixin):
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/")
@ -546,11 +474,7 @@ def change_status_event(request, pk, status):
if request.method == "POST":
event.status = Event.STATUS(status)
event.save(update_fields=["status"])
with_msg = event.notify_if_required(request)
if with_msg:
messages.success(request, _("The status has been successfully modified and a message has been sent to the person who proposed the event."))
else:
messages.success(request, _("The status has been successfully modified."))
messages.success(request, _("The status has been successfully modified."))
return HttpResponseRedirect(event.get_absolute_url())
@ -569,7 +493,7 @@ def import_event_proxy(request):
class EventCreateView(SuccessMessageMixin, CreateView):
model = Event
form_class = EventFormWithContact
form_class = EventForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@ -593,32 +517,31 @@ 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()
if form.cleaned_data.get("email") or form.cleaned_data.get("comments"):
has_comments = not form.cleaned_data.get("comments") in ["", None]
form.instance.set_message(
Message(subject=_('during the creation process'),
message=form.cleaned_data.get("comments"),
email=form.cleaned_data.get("email"),
closed=False,
message_type=Message.TYPE.FROM_CONTRIBUTOR if has_comments else Message.TYPE.FROM_CONTRIBUTOR_NO_MSG))
form.instance.import_sources = None
form.instance.set_processing_user(self.request.user)
result = super().form_valid(form)
if form.cleaned_data['cloning']:
with_msg = form.instance.notify_if_required(self.request)
if with_msg:
messages.success(self.request, _("A message has been sent to the person who proposed the initial event."))
return result
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
@ -685,14 +608,11 @@ class URLEventEvaluation:
def import_from_urls(request):
URLSubmissionFormSet = formset_factory(URLSubmissionForm, extra=9, min_num=1)
if request.method == "POST":
formset = URLSubmissionFormSet(request.POST, request.FILES)
if not request.user.is_authenticated:
contactform = SimpleContactForm(request.POST)
if formset.is_valid() and (request.user.is_authenticated or contactform.is_valid()):
if formset.is_valid():
# evaluate all the forms
ucat = [URLEventEvaluation(form, request.user.is_authenticated) for form in formset.forms]
@ -719,32 +639,20 @@ def import_from_urls(request):
request,
_('Integrating {} url(s) into our import process.').format(len(ucat))
)
email = None
comments = None
if not request.user.is_authenticated:
email = contactform.cleaned_data["email"]
comments = contactform.cleaned_data["comments"]
import_events_from_urls.delay(ucat,
user_id=request.user.pk if request.user else None,
email=email, comments=comments)
import_events_from_urls.delay(ucat)
return HttpResponseRedirect(reverse("thank_you"))
else:
return HttpResponseRedirect(reverse("home"))
else:
formset = URLSubmissionFormSet()
if not request.user.is_authenticated:
contactform = SimpleContactForm()
context = {"formset": formset}
if not request.user.is_authenticated:
context["contactform"] = contactform
return render(request, "agenda_culturel/import_set.html", context=context)
return render(request, "agenda_culturel/import_set.html", context={"formset": formset})
def import_from_url(request):
form = URLSubmissionFormWithContact(is_authenticated=request.user.is_authenticated)
form = URLSubmissionForm()
initial = {
"start_day": date.today() + timedelta(days=1),
@ -756,7 +664,7 @@ def import_from_url(request):
# if the form has been sent
if request.method == "POST":
form = URLSubmissionFormWithContact(request.POST, is_authenticated=request.user.is_authenticated)
form = URLSubmissionForm(request.POST)
# if the form is valid
if form.is_valid():
@ -781,7 +689,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, user_id=request.user.pk if request.user else None, email=form.cleaned_data.get("email"), comments=form.cleaned_data.get("comments"))
import_events_from_url.delay(uc.url, uc.cat, uc.tags)
return HttpResponseRedirect(reverse("thank_you"))
@ -799,7 +707,7 @@ def export_event_ical(request, year, month, day, pk):
events = list()
events.append(event)
cal = Event.export_to_ics(events, request)
cal = Event.export_to_ics(events)
response = HttpResponse(content_type="text/calendar")
response.content = cal.to_ical().decode("utf-8").replace("\r\n", "\n")
@ -809,17 +717,14 @@ 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)
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
calendar = CalendarList(now + timedelta(days=-7), now + timedelta(days=+60), filter)
ical = calendar.export_to_ics()
response = HttpResponse(content_type="text/calendar")
response.content = ical.to_ical().decode("utf-8").replace("\r\n", "\n")
@ -830,71 +735,41 @@ def export_ical(request):
return response
@method_decorator(check_honeypot, name='post')
class MessageCreateView(SuccessMessageMixin, CreateView):
model = Message
template_name = "agenda_culturel/message_create_form.html"
form_class = MessageForm
class ContactMessageCreateView(SuccessMessageMixin, CreateView):
model = ContactMessage
template_name = "agenda_culturel/contactmessage_create_form.html"
fields = ["subject", "name", "email", "message"]
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()
return form_class(**self.get_form_kwargs())
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):
if self.request.user.is_authenticated:
form.instance.user = self.request.user
form.instance.message_type = Message.TYPE.EVENT_REPORT if "pk" in self.kwargs else Message.TYPE.CONTACT_FORM
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 MessageDeleteView(SuccessMessageMixin, DeleteView):
model = Message
class ContactMessageDeleteView(SuccessMessageMixin, DeleteView):
model = ContactMessage
success_message = _(
"The contact message has been successfully deleted."
)
success_url = reverse_lazy("messages")
success_url = reverse_lazy("contactmessages")
class MessageUpdateView(
class ContactMessageUpdateView(
SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView
):
model = Message
permission_required = "agenda_culturel.change_message"
template_name = "agenda_culturel/message_moderation_form.html"
model = ContactMessage
permission_required = "agenda_culturel.change_contactmessage"
template_name = "agenda_culturel/contactmessage_moderation_form.html"
fields = ("spam", "closed", "comments")
success_message = _(
"The contact message properties has been successfully modified."
)
success_url = reverse_lazy("messages")
success_url = reverse_lazy("contactmessages")
def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""
@ -931,27 +806,23 @@ 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 = [today]
days = [date.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").select_related("exact_location", "category")[:5]
events = Event.objects.all().order_by("-created_date")[:5]
# get last batch imports
batch_imports = BatchImportation.objects.all().select_related("recurrentImport").order_by("-created_date")[:5]
batch_imports = BatchImportation.objects.all().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])
)
@ -967,41 +838,13 @@ 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_not_moderated": nb_not_moderated},
"nb_running": nb_running, "nb_all": nb_all},
)
@ -1030,15 +873,15 @@ def recent(request):
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.view_message")
def view_messages(request):
filter = MessagesFilterAdmin(
request.GET, queryset=Message.objects.all().order_by("-date")
@permission_required("agenda_culturel.view_contactmessage")
def contactmessages(request):
filter = ContactMessagesFilterAdmin(
request.GET, queryset=ContactMessage.objects.all().order_by("-date")
)
paginator = PaginatorFilter(filter, 10, request)
page = request.GET.get("page")
nb_spams = Message.objects.filter(spam=True).count()
nb_spams = ContactMessage.objects.filter(spam=True).count()
try:
response = paginator.page(page)
@ -1049,24 +892,24 @@ def view_messages(request):
return render(
request,
"agenda_culturel/messages.html",
"agenda_culturel/contactmessages.html",
{"filter": filter, "nb_spams": nb_spams, "paginator_filter": response},
)
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.view_message")
@permission_required("agenda_culturel.view_contactmessage")
def delete_cm_spam(request):
if request.method == "POST":
Message.objects.filter(spam=True).delete()
ContactMessage.objects.filter(spam=True).delete()
messages.success(request, _("Spam has been successfully deleted."))
return HttpResponseRedirect(reverse_lazy("messages"))
return HttpResponseRedirect(reverse_lazy("contactmessages"))
else:
nb_msgs = Message.objects.values('spam').annotate(total=Count('spam'))
nb_msgs = ContactMessage.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("messages")
cancel_url = reverse_lazy("contactmessages")
return render(
request,
"agenda_culturel/delete_spams_confirm.html",
@ -1622,7 +1465,6 @@ 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)
@ -1998,7 +1840,6 @@ class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateVi
class PlaceFromEventCreateView(PlaceCreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["event"] = self.event
@ -2009,14 +1850,6 @@ 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):
@ -2138,10 +1971,10 @@ def view_tag(request, t):
except EmptyPage:
response = paginator.page(paginator.num_pages)
rimports = RecurrentImport.objects.filter(defaultTags__contains=[t])
tag = Tag.objects.filter(name=t).first()
context = {"tag": t, "paginator_filter": response, "object": tag, "rimports": rimports}
context = {"tag": t, "paginator_filter": response, "object": tag}
return render(request, "agenda_culturel/tag.html", context)
@ -2204,21 +2037,11 @@ def rename_tag(request, t):
e.tags += [new_name]
Event.objects.bulk_update(events, fields=["tags"])
# find all recurrent imports and fix them
rimports = RecurrentImport.objects.filter(defaultTags__contains=[t])
for ri in rimports:
ri.tags = [te for te in ri.defaultTags if te != t]
if not new_name in ri.tags:
ri.tags += [new_name]
RecurrentImport.objects.bulk_update(rimports, fields=["defaultTags"])
# find tag object
tag_object = Tag.objects.filter(name=t).first()
if tag_object:
tag_object.name = new_name
tag_object.save()
messages.success(
request,
(_(
@ -2237,6 +2060,8 @@ def rename_tag(request, t):
@permission_required("agenda_culturel.delete_tag")
def delete_tag(request, t):
respage = reverse_lazy("view_all_tags")
nb = Event.objects.filter(tags__contains=[t]).count()
obj = Tag.objects.filter(name=t).first()
if request.method == "POST":
@ -2246,12 +2071,6 @@ def delete_tag(request, t):
e.tags = [te for te in e.tags if te != t]
Event.objects.bulk_update(events, fields=["tags"])
# remove tag from recurrent imports
rimports = RecurrentImport.objects.filter(defaultTags__contains=[t])
for ri in rimports:
ri.tags = [te for te in ri.defaultTags if te != t]
RecurrentImport.objects.bulk_update(rimports, fields=["defaultTags"])
# find tag object
tag_object = Tag.objects.filter(name=t).first()
if tag_object:
@ -2265,26 +2084,11 @@ def delete_tag(request, t):
)
return HttpResponseRedirect(respage)
else:
nb = Event.objects.filter(tags__contains=[t]).count()
obj = Tag.objects.filter(name=t).first()
nbi = RecurrentImport.objects.filter(defaultTags__contains=[t]).count()
cancel_url = request.META.get("HTTP_REFERER", "")
if cancel_url == "":
cancel_url = respage
return render(
request,
"agenda_culturel/tag_confirm_delete_by_name.html",
{"tag": t, "nb": nb, "nbi": nbi, "cancel_url": cancel_url, "obj": obj},
{"tag": t, "nb": nb, "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,5 +42,4 @@ 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
django-honeypot==1.2.1
emoji==2.14.0

View File

@ -289,7 +289,6 @@
{
"com_name": "Clermont-Ferrand",
"main": 10,
"suggested": 10,
"geo_point_2d": {
"lon": 3.1153994509459313,
"lat": 45.78590931605406
@ -340,7 +339,6 @@
{
"com_name": "Riom",
"main": 3,
"suggested": 10,
"geo_point_2d": {
"lon": 3.13259085594027,
"lat": 45.89435053196184

View File

@ -13,10 +13,7 @@ def run():
# remove all locations
ReferenceLocation.objects.all().delete()
objs = [ReferenceLocation(location=Point(c["geo_point_2d"]["lon"], c["geo_point_2d"]["lat"]),
name=c["com_name"],
main=c["main"] if "main" in c else 0,
suggested_distance=c["suggested"] if "suggested" in c else None) for c in data]
objs = [ReferenceLocation(location=Point(c["geo_point_2d"]["lon"], c["geo_point_2d"]["lat"]), name=c["com_name"], main=c["main"] if "main" in c else 0) for c in data]
objs = ReferenceLocation.objects.bulk_create(objs, ignore_conflicts=True)

View File

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