From f59b463c64606a5fb68a44fa23259b2b3ba4eedd Mon Sep 17 00:00:00 2001 From: SebF Date: Sun, 23 Mar 2025 13:19:06 +0100 Subject: [PATCH 01/20] Downgrade version minimale pour pre-commit --- .pre-commit-config.yaml | 2 +- src/agenda_culturel/views.py | 3076 ---------------------------------- 2 files changed, 1 insertion(+), 3077 deletions(-) delete mode 100644 src/agenda_culturel/views.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 79c5ddb..99fe2e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.13 + python: python3.12 repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror diff --git a/src/agenda_culturel/views.py b/src/agenda_culturel/views.py deleted file mode 100644 index a44a7c1..0000000 --- a/src/agenda_culturel/views.py +++ /dev/null @@ -1,3076 +0,0 @@ -import calendar as _calendar -import hashlib -import logging -from datetime import date, timedelta - -import emoji -from django.contrib import messages -from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.mixins import ( - LoginRequiredMixin, - PermissionRequiredMixin, - UserPassesTestMixin, -) -from django.contrib.messages.views import SuccessMessageMixin -from django.core.cache import cache -from django.core.mail import mail_admins -from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.db.models import Count, F, Func, OuterRef, Q, Subquery -from django.db.models.functions import TruncMonth -from django.http import ( - Http404, - HttpResponse, - HttpResponseForbidden, - HttpResponseRedirect, -) -from django.shortcuts import get_object_or_404, render -from django.urls import reverse, reverse_lazy -from django.utils.decorators import method_decorator -from django.utils.html import escape -from django.utils.safestring import mark_safe -from django.utils.timezone import datetime -from django.utils.translation import gettext_lazy as _ -from django.views.generic import DetailView, ListView -from django.views.generic.edit import ( - CreateView, - DeleteView, - ModelFormMixin, - UpdateView, -) -from honeypot.decorators import check_honeypot -from django.db.models.aggregates import StdDev -from django.db.models import Avg, Max, Min -from django.db.models import Aggregate, FloatField -from django.db.models.functions import ExtractDay - -from .calendar import CalendarDay, CalendarList, CalendarMonth, CalendarWeek -from .celery import app as celery_app -from .celery import ( - import_events_from_json, - import_events_from_url, - import_events_from_urls, - run_all_recurrent_imports, - run_all_recurrent_imports_canceled, - run_all_recurrent_imports_failed, - run_recurrent_import, - update_orphan_pure_import_events, -) -from .filters import ( - DuplicatedEventsFilter, - EventFilter, - EventFilterAdmin, - MessagesFilterAdmin, - RecurrentImportFilter, - SearchEventFilter, - SimpleSearchEventFilter, -) -from .forms import ( - BatchImportationForm, - CategorisationForm, - CategorisationRuleImportForm, - EventAddPlaceForm, - EventForm, - EventFormWithContact, - EventModerateForm, - FixDuplicates, - MergeDuplicates, - MessageEventForm, - MessageForm, - PlaceForm, - RecurrentImportForm, - SelectEventInList, - SimpleContactForm, - TagForm, - TagRenameForm, - URLSubmissionFormSet, - URLSubmissionFormWithContact, - UserProfileForm, -) -from .import_tasks.extractor import Extractor -from .models import ( - BatchImportation, - CategorisationRule, - Category, - DuplicatedEvents, - Event, - Message, - Organisation, - Place, - RecurrentImport, - StaticContent, - Tag, - remove_accents, - UserProfile, -) -from .utils import PlaceGuesser - -logger = logging.getLogger(__name__) - - -class Median(Aggregate): - function = "PERCENTILE_CONT" - name = "median" - output_field = FloatField() - template = "%(function)s(0.5) WITHIN GROUP (ORDER BY %(expressions)s)" - - -class PaginatorFilter(Paginator): - def __init__(self, filter, nb, request): - self.request = request - self.filter = filter - - super().__init__(filter.qs, nb) - - self.url_first_page = PaginatorFilter.update_param( - self.request.get_full_path(), "page", 1 - ) - self.url_last_page = PaginatorFilter.update_param( - self.request.get_full_path(), "page", self.num_pages - ) - - def update_param(params, key, value): - p = params.split("?") - root = p[0] - if len(p) > 1: - other = p[1] - others = other.split("&") - others = [o for o in others if not o.startswith(key)] - others += [key + "=" + str(value)] - return root + "?" + "&".join(others) - else: - return root + "?" + key + "=" + str(value) - - def page(self, *args, **kwargs): - page = super().page(*args, **kwargs) - - try: - page.url_previous_page = PaginatorFilter.update_param( - self.request.get_full_path(), - "page", - page.previous_page_number(), - ) - except EmptyPage: - page.url_previous_page = self.request.get_full_path() - - try: - page.url_next_page = PaginatorFilter.update_param( - self.request.get_full_path(), "page", page.next_page_number() - ) - except EmptyPage: - page.url_next_page = self.request.get_full_path() - - return page - - -# -# -# Useful for translation -to_be_translated = [ - _("Recurrent import name"), - _("Add another"), - _("Browse..."), - _("No file selected."), -] - - -def get_event_qs(request): - if request.user.is_authenticated: - return Event.objects.filter() - else: - return Event.objects.filter(status=Event.STATUS.PUBLISHED) - - -def page_not_found(request, exception=None): - return render(request, "page-erreur.html", status=404, context={"error": 404}) - - -def internal_server_error(request): - try: - mail_admins( - request.site.name + _(": error 500"), - _("An internal error has occurred on site {} at address {}.").format( - request.site.name, request.build_absolute_uri() - ), - ) - except Exception: - pass - return render(request, "page-erreur.html", status=500, context={"error": 500}) - - -def thank_you(request): - return render(request, "agenda_culturel/thank_you.html") - - -def mentions_legales(request): - context = { - "title": "Mentions légales", - "static_content": "mentions_legales", - "url_path": reverse_lazy("mentions_legales"), - } - return render(request, "agenda_culturel/page-single.html", context) - - -def about(request): - rimports = ( - RecurrentImport.objects.filter(~Q(recurrence=RecurrentImport.RECURRENCE.NEVER)) - .order_by("name__unaccent") - .all() - ) - context = { - "title": "À propos", - "static_content": "about", - "url_path": reverse_lazy("about"), - "rimports": rimports, - } - return render(request, "agenda_culturel/page-rimports-list.html", context) - - -def moderation_rules(request): - context = { - "title": _("Moderation rules"), - "static_content": "moderation_rules", - "url_path": reverse_lazy("moderation_rules"), - } - return render(request, "agenda_culturel/page-single.html", context) - - -def import_requirements(request): - context = { - "title": _("Import requirements"), - "static_content": "import_requirements", - "url_path": reverse_lazy("import_requirements"), - } - return render(request, "agenda_culturel/page-single.html", context) - - -def home(request, cat=None): - return week_view(request, home=True, cat=cat) - - -def month_view(request, year=None, month=None, cat=None): - now = date.today() - if year is None and month is None: - day = now.day - else: - day = None - if year is None: - year = now.year - if month is None: - month = now.month - - request = EventFilter.set_default_values(request) - qs = get_event_qs(request).only( - "title", - "start_day", - "start_time", - "category", - "other_versions", - "recurrences", - "end_day", - "end_time", - "uuids", - "status", - "tags", - ) - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - - filter = EventFilter(request.GET, qs, request=request) - - if filter.has_category_parameters(): - return HttpResponseRedirect(filter.get_new_url()) - - cmonth = CalendarMonth(year, month, filter, day=day) - - context = { - "calendar": cmonth, - "this_month": day is not None, - "filter": filter, - "category": category, - "init_date": now if cmonth.today_in_calendar() else cmonth.firstdate, - } - return render(request, "agenda_culturel/page-month.html", context) - - -def week_view(request, year=None, week=None, home=False, cat=None): - now = date.today() - if year is None: - year = now.isocalendar()[0] - if week is None: - week = now.isocalendar()[1] - - request = EventFilter.set_default_values(request) - qs = ( - get_event_qs(request) - .select_related("exact_location") - .only( - "title", - "start_day", - "start_time", - "category", - "other_versions", - "recurrences", - "end_day", - "end_time", - "uuids", - "status", - "tags", - "local_image", - "image", - "image_alt", - "exact_location", - "description", - ) - ) - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - filter = EventFilter(request.GET, qs, request=request) - - if filter.has_category_parameters(): - return HttpResponseRedirect(filter.get_new_url()) - - cweek = CalendarWeek(year, week, filter) - - context = { - "year": year, - "week": week, - "calendar": cweek, - "filter": filter, - "category": category, - "init_date": now if cweek.today_in_calendar() else cweek.firstdate, - } - if home: - context["home"] = 1 - return render(request, "agenda_culturel/page-week.html", context) - - -def day_view(request, year=None, month=None, day=None, cat=None): - if year is None or month is None or day is None: - if "when" in request.POST: - when = datetime.strptime(request.POST["when"], "%Y-%m-%d") - year = when.year - month = when.month - day = when.day - - request = EventFilter.set_default_values(request) - qs = get_event_qs(request).select_related("exact_location") - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - filter = EventFilter(request.GET, qs, request=request) - - return HttpResponseRedirect( - reverse_lazy("day_view", args=[year, month, day]) - + "?" - + filter.get_url() - ) - - return upcoming_events(request, year, month, day, 0, cat) - - -def upcoming_events(request, year=None, month=None, day=None, neighsize=1, cat=None): - now = date.today() - if year is None: - year = now.year - if month is None: - month = now.month - if day is None: - day = now.day - - day = date(year, month, day) - day = day + timedelta(days=neighsize) - - request = EventFilter.set_default_values(request) - qs = get_event_qs(request).select_related("exact_location") - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - - filter = EventFilter(request.GET, qs, request=request) - - if filter.has_category_parameters(): - return HttpResponseRedirect(filter.get_new_url()) - - cal = CalendarList( - day + timedelta(days=-neighsize), - day + timedelta(days=neighsize), - filter, - True, - ) - - context = { - "calendar": cal, - "now": now, - "day": day, - "init_date": now if cal.today_in_calendar() else day, - "filter": filter, - "date_pred": day + timedelta(days=-neighsize - 1), - "date_next": day + timedelta(days=neighsize + 1), - "category": category, - } - - return render(request, "agenda_culturel/page-upcoming.html", context) - - -class StaticContentCreateView(LoginRequiredMixin, CreateView): - model = StaticContent - fields = ["text"] - permission_required = "agenda_culturel.add_staticcontent" - - def form_valid(self, form): - form.instance.name = self.request.GET["name"] - form.instance.url_path = self.request.GET["url_path"] - return super().form_valid(form) - - -class StaticContentUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = StaticContent - permission_required = "agenda_culturel.change_staticcontent" - fields = ["text"] - success_message = _("The static content has been successfully updated.") - - -def update_from_source(request, pk): - event = get_object_or_404(Event, pk=pk) - - url = event.get_updateable_uuid() - 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, - ) - messages.success( - request, - _("The event update has been queued and will be completed shortly."), - ) - - return HttpResponseRedirect(event.get_absolute_url()) - - -class EventUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = Event - permission_required = "agenda_culturel.change_event" - form_class = EventForm - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["is_authenticated"] = self.request.user.is_authenticated - kwargs["is_moderation_expert"] = ( - self.request.user.userprofile.is_moderation_expert - ) - kwargs["is_cloning"] = self.is_cloning - kwargs["is_edit_from_moderation"] = self.is_edit_force() - 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") and self.with_msg - else "" - ) - return mark_safe(_("The event has been successfully modified.") + txt) - - def is_edit_force(self): - return "edit-force" in self.request.path.split("/") - - def get_object(self, queryset=None): - event = super().get_object(queryset) - if event.status == Event.STATUS.DRAFT: - event.status = Event.STATUS.PUBLISHED - if self.is_edit_force(): - event.free_modification_lock(self.request.user) - return event - - 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("/") - if self.is_cloning: - messages.info( - self.request, - _( - "Changes will be visible on a local copy of the event. The version identical to the imported source will be hidden." - ), - ) - self.is_simple_cloning = "simple-clone" in self.request.path.split("/") - result = super().get_initial() - - if self.is_cloning and "other_versions" not in result: - obj = self.get_object() - # if no DuplicatedEvents is associated, create one - obj.other_versions = DuplicatedEvents.objects.create() - obj.other_versions.save() - # save them without updating modified date - obj.set_no_modification_date_changed() - 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 - result["simple_cloning"] = True - - if self.is_cloning or self.is_simple_cloning: - obj = self.get_object() - if obj.local_image: - result["old_local_image"] = obj.local_image.name - - return result - - def get_success_url(self): - if "save_and_next" in self.request.POST: - return reverse_lazy("moderate_event_next", args=[self.object.pk]) - else: - return self.object.get_absolute_url() - - -class EventModerateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = Event - permission_required = "agenda_culturel.change_event" - template_name = "agenda_culturel/event_form_moderate.html" - form_class = EventModerateForm - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["is_moderation_expert"] = ( - self.request.user.userprofile.is_moderation_expert - ) - 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") and self.with_msg - else "" - ) - return mark_safe( - _('The event {} has been moderated with success.').format( - self.object.get_absolute_url(), self.object.title - ) - + txt - ) - - def is_moderate_next(self): - return "after" in self.request.path.split("/") - - def is_moderate_back(self): - return "back" in self.request.path.split("/") - - def is_moderate_force(self): - return "moderate-force" in self.request.path.split("/") - - def is_starting_moderation(self): - return "pk" not 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, opk): - # 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)) - ) - ) - else: - qs = qs.filter(Q(start_day__gte=start_day) & ~Q(pk=opk)) - - # get only possibly representative events - qs = qs.filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - - # remove trash events - qs = qs.filter(~Q(status=Event.STATUS.TRASH)) - - # sort by datetime - qs = qs.order_by("start_day", "start_time") - - return qs.first() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - if self.is_moderate_next(): - context["pred"] = self.kwargs["pred"] - return context - - def get_object(self, queryset=None): - if self.is_starting_moderation(): - now = datetime.now() - event = EventModerateView.get_next_event(now.date(), now.time(), None) - else: - event = super().get_object(queryset) - if event.status == Event.STATUS.DRAFT: - event.status = Event.STATUS.PUBLISHED - if self.is_moderate_back(): - pred = Event.objects.filter(pk=self.kwargs["pred"]).first() - pred.free_modification_lock(self.request.user) - if self.is_moderate_force(): - event.free_modification_lock(self.request.user, False) - return event - - def post(self, request, *args, **kwargs): - try: - return super().post(request, args, kwargs) - except Http404: - return HttpResponseRedirect( - reverse_lazy("error_next_event", args=[self.object.pk]) - ) - - 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_edit_local" in self.request.POST: - return reverse_lazy("edit_event", args=[self.object.get_local_version().pk]) - else: - return self.object.get_absolute_url() - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.change_event") -def error_next_event(request, pk): - obj = Event.objects.filter(pk=pk).first() - - return render( - request, - "agenda_culturel/event_next_error_message.html", - {"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() - # free lock - obj.free_modification_lock(request.user) - start_day = obj.start_day - start_time = obj.start_time - - next_obj = EventModerateView.get_next_event(start_day, start_time, pk) - 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, None) - return HttpResponseRedirect(reverse_lazy("moderate_event", args=[obj.pk])) - - -class EventDeleteView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - DeleteView, -): - model = Event - permission_required = "agenda_culturel.delete_event" - success_url = reverse_lazy("recent") - success_message = _("The event has been successfully deleted.") - - -class EventDetailView(UserPassesTestMixin, DetailView, ModelFormMixin): - model = Event - form_class = MessageEventForm - template_name = "agenda_culturel/page-event.html" - queryset = ( - Event.objects.select_related("exact_location") - .select_related("category") - .select_related("other_versions") - .select_related("other_versions__representative") - .prefetch_related("message_set") - ) - - def test_func(self): - return ( - self.request.user.is_authenticated - or self.get_object().status == Event.STATUS.PUBLISHED - ) - - def get_object(self): - o = super().get_object() - o.download_missing_image() - if "year" in self.kwargs: - y = self.kwargs["year"] - m = self.kwargs["month"] - d = self.kwargs["day"] - obj = o.get_recurrence_at_date(y, m, d) - obj.set_current_date(date(y, m, d)) - return obj - else: - return o - - 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/") -@permission_required("agenda_culturel.change_event") -def change_status_event(request, pk, status): - event = get_object_or_404(Event, pk=pk) - - if request.method == "POST": - event.status = Event.STATUS(status) - fields = ["status", "moderated_date", "moderated_by_user"] - event.set_in_moderation_process() - event.update_modification_dates() - event.save(update_fields=fields) - 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.")) - - return HttpResponseRedirect(event.get_absolute_url()) - - else: - cancel_url = request.META.get("HTTP_REFERER", "") - if cancel_url == "": - cancel_url = reverse_lazy("home") - return render( - request, - "agenda_culturel/event_confirm_change_status.html", - {"status": status, "event": event, "cancel_url": cancel_url}, - ) - - -def import_event_proxy(request): - return render(request, "agenda_culturel/event_import.html") - - -class EventCreateView(SuccessMessageMixin, CreateView): - model = Event - form_class = EventFormWithContact - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["is_authenticated"] = self.request.user.is_authenticated - return kwargs - - def get_success_url(self): - if self.request.user.is_authenticated: - if "save_and_next" in self.request.POST: - return reverse_lazy("moderate_event_next", args=[self.object.pk]) - else: - return self.object.get_absolute_url() - else: - return reverse_lazy("home") - - def get_success_message(self, cleaned_data): - if self.request.user.is_authenticated: - return mark_safe( - _('The event was created: {}.').format( - self.object.get_absolute_url(), self.object.title - ) - ) - else: - return _( - "The event has been submitted and will be published as soon as it has been validated by the moderation team." - ) - - def form_valid(self, form): - 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 = form.cleaned_data.get("comments") not in ["", None] - form.instance.add_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 - - -# A class to evaluate the URL according to the existing events and the authentification -# level of the user -class URLEventEvaluation: - def __init__(self, form, is_authenticated): - self.form = form - self.is_authenticated = is_authenticated - - self.cat = None - self.tags = [] - self.existing = None - self.url = form.cleaned_data.get("url") - self.event = None - if self.url is not None: - self.url = Extractor.clean_url(self.url) - # we check if the url is known - self.existing = Event.objects.filter(uuids__contains=[self.url]) - # if it's unknown - if len(self.existing) == 0: - self.existing = None - self.cat = form.cleaned_data.get("category") - if self.cat is not None: - self.cat = self.cat.name - self.tags = form.cleaned_data.get("tags") - - else: - published = [ - e for e in self.existing if e.status == Event.STATUS.PUBLISHED - ] - - if self.is_authenticated or len(published) > 1: - self.event = ( - published[0] if len(published) > 1 else self.existing[0] - ) - else: - self.event = None - - def exists(self): - return self.url is not None - - def is_new(self): - return self.exists() and self.existing is None - - def is_event_visible(self): - return self.event is not None - - def get_event(self): - if self.event is None: - return None - else: - return self.event - - def get_link(self): - e = self.get_event() - if e is None: - return "" - else: - return '' + escape(e.title) + "" - - def to_list(self): - if self.is_new(): - return (self.url, self.cat, self.tags) - - -def import_from_urls(request): - 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() - ): - # evaluate all the forms - ucat = [ - URLEventEvaluation(form, request.user.is_authenticated) - for form in formset.forms - ] - - # for each not new, add a message - for uc in ucat: - if uc.exists() and not uc.is_new(): - if uc.is_event_visible(): - messages.info( - request, - mark_safe( - _( - "{} has not been submitted since it" - "s already known: {}." - ).format(uc.url, uc.get_link()) - ), - ) - else: - messages.info( - request, - _( - "{} has not been submitted since it" - "s already known and currently into moderation process." - ).format(uc.url), - ) - - # keep only new ones - ucat = [uc.to_list() for uc in ucat if uc.is_new()] - - # finally process them or go back to home page - if len(ucat) > 0: - messages.info( - 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, - ) - 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) - - -def import_from_url(request): - form = URLSubmissionFormWithContact(is_authenticated=request.user.is_authenticated) - - initial = { - "start_day": date.today() + timedelta(days=1), - "start_time": "20:00", - "end_time": "22:00", - } - - form_event = EventForm(initial=initial) - - # if the form has been sent - if request.method == "POST": - form = URLSubmissionFormWithContact( - request.POST, is_authenticated=request.user.is_authenticated - ) - - # if the form is valid - if form.is_valid(): - uc = URLEventEvaluation(form, request.user.is_authenticated) - - if uc.exists() and not uc.is_new(): - if uc.is_event_visible(): - messages.info( - request, - mark_safe( - _( - "{} has not been submitted since its already known: {}." - ).format(uc.url, uc.get_link()) - ), - ) - return HttpResponseRedirect(uc.get_event().get_absolute_url()) - else: - messages.info( - request, - _( - "{} has not been submitted since it" - "s already known and currently into moderation process." - ).format(uc.url), - ) - return HttpResponseRedirect(reverse("home")) - - else: - messages.info( - 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"), - ) - return HttpResponseRedirect(reverse("thank_you")) - - return render( - request, - "agenda_culturel/import.html", - context={"form": form, "form_event": form_event}, - ) - - -def export_event_ical(request, year, month, day, pk): - event = get_object_or_404(Event, pk=pk) - event = event.get_recurrence_at_date(year, month, day) - - events = list() - events.append(event) - - cal = Event.export_to_ics(events, request) - - response = HttpResponse(content_type="text/calendar") - response.content = cal.to_ical().decode("utf-8").replace("\r\n", "\n") - response["Content-Disposition"] = "attachment; filename={0}{1}".format( - event.title.replace("\n", " ").replace("\r", "")[0:32], ".ics" - ) - - return response - - -def export_ical(request, cat=None, tag=None, organisation_pk=None, place_pk=None): - now = date.today() - - qs = get_event_qs(request) - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - - if place_pk is not None: - qs = qs.filter(exact_location=place_pk) - if organisation_pk is not None: - qs = qs.filter(organisers__in=[organisation_pk]) - if tag is not None: - qs = qs.filter(tags__in=[tag]) - - request = EventFilter.set_default_values(request) - filter = EventFilter(request.GET, queryset=qs, request=request) - - if filter.has_category_parameters(): - return HttpResponseRedirect(filter.get_new_url()) - - id_cache = hashlib.md5( - ( - filter.get_url() - + "-" - + str(tag) - + "-" - + str(cat) - + "-" - + str(organisation_pk) - + "-" - + str(place_pk) - ).encode("utf8") - ).hexdigest() - ical = cache.get(id_cache) - if not ical: - calendar = CalendarList( - now + timedelta(days=-7), now + timedelta(days=+60), filter - ) - ical = calendar.export_to_ics(request) - cache.set(id_cache, ical, 3600) # 1 heure - - response = HttpResponse(content_type="text/calendar") - response.content = ical.to_ical().decode("utf-8").replace("\r\n", "\n") - extra = filter.to_str(" ") - if extra is None: - extra = "" - if category is not None: - extra += " " + str(category) - if place_pk is not None: - extra += ( - " @ " + Place.objects.filter(pk=place_pk).values("name").first()["name"] - ) - if organisation_pk is not None: - extra += ( - " - " - + Organisation.objects.filter(pk=organisation_pk) - .values("name") - .first()["name"] - ) - if tag is not None: - extra += " - " + emoji.replace_emoji(tag, replace="") - - response["Content-Disposition"] = "attachment; filename={0}{1}{2}".format( - "Pommes de lune", extra, ".ics" - ) - - 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 - - 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 - success_message = _("The contact message has been successfully deleted.") - success_url = reverse_lazy("messages") - - -class MessageUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = Message - permission_required = "agenda_culturel.change_message" - template_name = "agenda_culturel/message_moderation_form.html" - fields = ("spam", "closed", "comments") - - success_message = _( - "The contact message properties has been successfully modified." - ) - - success_url = reverse_lazy("messages") - - def get_form_kwargs(self): - """Return the keyword arguments for instantiating the form.""" - kwargs = super().get_form_kwargs() - if hasattr(self, "object"): - kwargs.update({"instance": self.object}) - return kwargs - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_event") -def activite(request): - now = date.today() - - days = [now] - while len(days) < 7 or days[-1].weekday() != 0: - days.append(days[-1] + timedelta(days=-1)) - - weeks = [days[-1]] - for w in range(0, 8): - weeks.append(weeks[-1] + timedelta(days=-7)) - - daily_modifications = Event.get_count_modifications([(d, 1) for d in days]) - weekly_modifications = Event.get_count_modifications([(w, 7) for w in weeks]) - - return render( - request, - "agenda_culturel/page-activity.html", - { - "daily_modifications": daily_modifications, - "weekly_modifications": weekly_modifications, - }, - ) - - -@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() - - # get information about recent modifications - days = [today] - for i in range(0, 2): - days.append(days[-1] + timedelta(days=-1)) - daily_modifications = Event.get_count_modifications([(d, 1) for d in days]) - - # get last created events - events = ( - Event.objects.all() - .order_by("-created_date") - .select_related("exact_location", "category")[:5] - ) - - # get last batch imports - rel_event = Event.objects.filter( - import_sources__contains=[OuterRef("url_source")] - ).values("pk")[:1] - batch_imports = ( - BatchImportation.objects.all() - .select_related("recurrentImport") - .annotate(event_id=Subquery(rel_event)) - .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]) - ) - - nb_failed = imported_events.filter( - last_run_status=BatchImportation.STATUS.FAILED - ).count() - nb_canceled = imported_events.filter( - last_run_status=BatchImportation.STATUS.CANCELED - ).count() - nb_running = imported_events.filter( - last_run_status=BatchImportation.STATUS.RUNNING - ).count() - nb_all = imported_events.count() - - # get some info about imported (or not) events - srcs = RecurrentImport.objects.all().values_list("source") - in_future = Event.objects.filter(Q(start_day__gte=today)) - nb_in_rimport = in_future.filter(Q(import_sources__overlap=srcs)).count() - nb_in_orphan_import = in_future.filter( - ( - Q(import_sources__isnull=False) - & (Q(modified_date__isnull=True) | Q(modified_date__lte=F("imported_date"))) - ) - & ~Q(import_sources__overlap=srcs) - ).count() - - # get all non moderated events - nb_not_moderated = Event.get_nb_not_moderated(today, nb_mod_days, nb_classes) - - 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_in_rimport": nb_in_rimport, - "nb_in_orphan_import": nb_in_orphan_import, - }, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_event") -def recent(request): - filter = EventFilterAdmin( - request.GET, queryset=Event.objects.all().order_by("-created_date") - ) - paginator = PaginatorFilter(filter, 10, request) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/recent.html", - {"filter": filter, "paginator_filter": response}, - ) - - -@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") - ) - paginator = PaginatorFilter(filter, 10, request) - page = request.GET.get("page") - - nb_spams = Message.objects.filter(spam=True).count() - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/messages.html", - {"filter": filter, "nb_spams": nb_spams, "paginator_filter": response}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_message") -def delete_cm_spam(request): - if request.method == "POST": - Message.objects.filter(spam=True).delete() - - messages.success(request, _("Spam has been successfully deleted.")) - return HttpResponseRedirect(reverse_lazy("messages")) - else: - nb_msgs = Message.objects.values("spam").annotate(total=Count("spam")) - nb_total = sum([nb["total"] for nb in nb_msgs]) - nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]]) - cancel_url = reverse_lazy("messages") - return render( - request, - "agenda_culturel/delete_spams_confirm.html", - { - "nb_total": nb_total, - "nb_spams": nb_spams, - "cancel_url": cancel_url, - }, - ) - - -def event_search(request, full=False): - categories = None - tags = None - places = None - organisations = None - rimports = None - - qs = get_event_qs(request).order_by("-start_day") - if not request.user.is_authenticated: - qs = qs.filter( - ( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - ) - if full: - filter = SearchEventFilter( - request.GET, - queryset=qs, - request=request, - ) - else: - filter = SimpleSearchEventFilter( - request.GET, - queryset=qs, - request=request, - ) - if "q" in request.GET: - categories = Category.objects.filter(name__icontains=request.GET["q"]) - s_q = remove_accents(request.GET["q"].lower()) - tags = ( - Event.objects.extra( - where=["%s ILIKE ANY (tags)"], params=[request.GET["q"]] - ) - .annotate(arr_tags=Func(F("tags"), function="unnest")) - .values_list("arr_tags", flat=True) - .distinct() - ) - tags = [ - ( - t, - emoji.demojize(remove_accents(t).lower(), delimiters=("000", "")), - ) - for t in tags - ] - tags = [t for t in tags if s_q == t[1]] - tags.sort(key=lambda x: x[1]) - tags = [t[0] for t in tags] - places = Place.objects.filter( - Q(name__icontains=request.GET["q"]) - | Q(description__icontains=request.GET["q"]) - | Q(city__icontains=request.GET["q"]) - ) - organisations = Organisation.objects.filter( - Q(name__icontains=request.GET["q"]) - | Q(description__icontains=request.GET["q"]) - ) - if request.user.is_authenticated: - rimports = RecurrentImport.objects.filter( - name__icontains=request.GET["q"] - ) - - paginator = PaginatorFilter(filter, 10, request) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/search.html", - { - "filter": filter, - "categories": categories, - "tags": tags, - "places": places, - "organisations": organisations, - "rimports": rimports, - "has_results": len(request.GET) != 0 - or (len(request.GET) > 1 and "page" in request.GET), - "paginator_filter": response, - "full": full, - }, - ) - - -def event_search_full(request): - return event_search(request, True) - - -######################### -## batch importations -######################### - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_batchimportation") -def imports(request): - rel_event = Event.objects.filter( - import_sources__contains=[OuterRef("url_source")] - ).values("pk")[:1] - paginator = Paginator( - BatchImportation.objects.all() - .order_by("-created_date") - .annotate(event_id=Subquery(rel_event)), - 30, - ) - page = request.GET.get("page") - - today = date.today() - - srcs = RecurrentImport.objects.all().values_list("source") - in_future = Event.objects.filter(Q(start_day__gte=today)) - nb_in_orphan_import = in_future.filter( - ( - Q(import_sources__isnull=False) - & (Q(modified_date__isnull=True) | Q(modified_date__lte=F("imported_date"))) - ) - & ~Q(import_sources__overlap=srcs) - ).count() - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/imports.html", - { - "paginator_filter": response, - "nb_in_orphan_import": nb_in_orphan_import, - }, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.add_batchimportation", - "agenda_culturel.run_batchimportation", - ] -) -def add_import(request): - form = BatchImportationForm() - - if request.method == "POST": - form = BatchImportationForm(request.POST) - - if form.is_valid(): - import_events_from_json.delay(form.data["json"]) - - messages.success(request, _("The import has been run successfully.")) - return HttpResponseRedirect(reverse_lazy("imports")) - - return render(request, "agenda_culturel/batchimportation_form.html", {"form": form}) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_batchimportation", - "agenda_culturel.run_batchimportation", - ] -) -def cancel_import(request, pk): - import_process = get_object_or_404(BatchImportation, pk=pk) - - if request.method == "POST": - celery_app.control.revoke(import_process.celery_id) - - import_process.status = BatchImportation.STATUS.CANCELED - import_process.save(update_fields=["status"]) - - messages.success(request, _("The import has been canceled.")) - return HttpResponseRedirect(reverse_lazy("imports")) - else: - cancel_url = reverse_lazy("imports") - return render( - request, - "agenda_culturel/cancel_import_confirm.html", - {"object": import_process, "cancel_url": cancel_url}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_batchimportation", - "agenda_culturel.run_batchimportation", - ] -) -def update_orphan_events(request): - if request.method == "POST": - # run recurrent import - update_orphan_pure_import_events.delay() - - messages.success(request, _("The orphan event update has been launched.")) - return HttpResponseRedirect(reverse_lazy("imports")) - else: - today = date.today() - - srcs = RecurrentImport.objects.all().values_list("source") - in_future = Event.objects.filter(Q(start_day__gte=today)) - nb_in_orphan_import = in_future.filter( - ( - Q(import_sources__isnull=False) - & ( - Q(modified_date__isnull=True) - | Q(modified_date__lte=F("imported_date")) - ) - ) - & ~Q(import_sources__overlap=srcs) - ).count() - return render( - request, - "agenda_culturel/run_orphan_imports_confirm.html", - {"nb_in_orphan_import": nb_in_orphan_import}, - ) - - -######################### -## recurrent importations -######################### - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_recurrentimport") -def recurrent_imports(request, status=None): - newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by( - "-created_date" - ) - - qs = ( - RecurrentImport.objects.all() - .annotate(last_run_status=Subquery(newest.values("status")[:1])) - .order_by("-pk") - ) - - nb_failed = qs.filter(last_run_status=BatchImportation.STATUS.FAILED).count() - nb_canceled = qs.filter(last_run_status=BatchImportation.STATUS.CANCELED).count() - nb_running = qs.filter(last_run_status=BatchImportation.STATUS.RUNNING).count() - nb_all = qs.count() - - if status is not None: - qs = qs.filter(last_run_status=status) - - filter = RecurrentImportFilter(request.GET, queryset=qs) - - paginator = PaginatorFilter(filter, 20, request) - - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/rimports.html", - { - "paginator_filter": response, - "filter": filter, - "nb_all": nb_all, - "nb_failed": nb_failed, - "nb_canceled": nb_canceled, - "nb_running": nb_running, - "status": status, - }, - ) - - -class RecurrentImportCreateView( - LoginRequiredMixin, PermissionRequiredMixin, CreateView -): - model = RecurrentImport - permission_required = "agenda_culturel.add_recurrentimport" - success_url = reverse_lazy("recurrent_imports") - form_class = RecurrentImportForm - - -class RecurrentImportUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = RecurrentImport - permission_required = "agenda_culturel.change_recurrentimport" - form_class = RecurrentImportForm - success_message = _("The recurrent import has been successfully modified.") - - -class RecurrentImportDeleteView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - DeleteView, -): - model = RecurrentImport - permission_required = "agenda_culturel.delete_recurrentimport" - success_url = reverse_lazy("recurrent_imports") - success_message = _("The recurrent import has been successfully deleted.") - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_recurrentimport", - "agenda_culturel.view_batchimportation", - ] -) -def view_rimport(request, pk): - obj = get_object_or_404(RecurrentImport, pk=pk) - paginator = Paginator( - BatchImportation.objects.filter(recurrentImport=pk).order_by("-created_date"), - 10, - ) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/page-rimport.html", - {"paginator_filter": response, "object": obj}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_recurrentimport", - "agenda_culturel.run_recurrentimport", - ] -) -def run_rimport(request, pk): - rimport = get_object_or_404(RecurrentImport, pk=pk) - - if request.method == "POST": - # run recurrent import - run_recurrent_import.delay(pk) - - messages.success(request, _("The import has been launched.")) - return HttpResponseRedirect(reverse_lazy("view_rimport", args=[pk])) - else: - return render( - request, - "agenda_culturel/run_rimport_confirm.html", - {"object": rimport}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_recurrentimport", - "agenda_culturel.run_recurrentimport", - ] -) -def run_all_rimports(request, status=None): - if request.method == "POST": - # run recurrent import - if status == BatchImportation.STATUS.FAILED: - run_all_recurrent_imports_failed.delay() - elif status == BatchImportation.STATUS.CANCELED: - run_all_recurrent_imports_canceled.delay() - else: - run_all_recurrent_imports.delay() - - messages.success(request, _("Imports has been launched.")) - return HttpResponseRedirect(reverse_lazy("recurrent_imports")) - else: - if status == BatchImportation.STATUS.FAILED: - return render(request, "agenda_culturel/run_failed_rimports_confirm.html") - elif status == BatchImportation.STATUS.CANCELED: - return render(request, "agenda_culturel/run_canceled_rimports_confirm.html") - else: - return render(request, "agenda_culturel/run_all_rimports_confirm.html") - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_recurrentimport", - "agenda_culturel.run_recurrentimport", - ] -) -def run_all_fb_rimports(request, status=None): - if request.method == "POST": - run_all_recurrent_imports.delay(True) - - messages.success(request, _("Facebook imports has been launched.")) - return HttpResponseRedirect(reverse_lazy("recurrent_imports")) - else: - return render(request, "agenda_culturel/run_all_fb_rimports_confirm.html") - - -######################### -## duplicated events -######################### - - -class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView): - model = DuplicatedEvents - template_name = "agenda_culturel/duplicate.html" - - -@login_required(login_url="/accounts/login/") -@permission_required( - ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] -) -def update_duplicate_event(request, pk, epk): - edup = get_object_or_404(DuplicatedEvents, pk=pk) - event = get_object_or_404(Event, pk=epk) - - form = MergeDuplicates(duplicates=edup, event=event) - - if request.method == "POST": - form = MergeDuplicates(request.POST, duplicates=edup) - if form.is_valid(): - for f in edup.get_items_comparison(): - if not f["similar"]: - selected = form.get_selected_events(f["key"]) - if selected is not None: - if isinstance(selected, list): - values = [ - x - for x in [getattr(s, f["key"]) for s in selected] - if x is not None - ] - if len(values) != 0: - if isinstance(values[0], str): - setattr(event, f["key"], "\n".join(values)) - else: - setattr(event, f["key"], sum(values, [])) - else: - if f["key"] == "organisers": - event.organisers.set(selected.organisers.all()) - else: - setattr( - event, - f["key"], - getattr(selected, f["key"]), - ) - if f["key"] == "image": - setattr( - event, - "local_image", - getattr(selected, "local_image"), - ) - - event.other_versions.fix(event) - event.save() - - messages.info(request, _("Update successfully completed.")) - return HttpResponseRedirect(event.get_absolute_url()) - - return render( - request, - "agenda_culturel/update_duplicate.html", - context={ - "form": form, - "object": edup, - "event_id": edup.get_event_index(event), - }, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] -) -def merge_duplicate(request, pk): - edup = get_object_or_404(DuplicatedEvents, pk=pk) - form = MergeDuplicates(duplicates=edup) - - if request.method == "POST": - form = MergeDuplicates(request.POST, duplicates=edup) - if form.is_valid(): - events = edup.get_duplicated() - - # build fields for the new event - new_event_data = {} - for f in edup.get_items_comparison(): - if f["similar"]: - new_event_data[f["key"]] = getattr(events[0], f["key"]) - else: - selected = form.get_selected_events(f["key"]) - if selected is None: - new_event_data[f["key"]] = None - elif isinstance(selected, list): - values = [ - x - for x in [getattr(s, f["key"]) for s in selected] - if x is not None - ] - if len(values) == 0: - new_event_data[f["key"]] = None - else: - if isinstance(values[0], str): - new_event_data[f["key"]] = "\n".join(values) - else: - new_event_data[f["key"]] = sum(values, []) - else: - new_event_data[f["key"]] = getattr(selected, f["key"]) - if f["key"] == "image" and "local_image" not in new_event_data: - new_event_data["local_image"] = getattr( - selected, "local_image" - ) - - organisers = new_event_data.pop("organisers", None) - # create a new event that merge the selected events - new_event = Event(**new_event_data) - new_event.status = Event.STATUS.PUBLISHED - new_event.other_versions = edup - new_event.save() - if organisers is not None: - new_event.organisers.set(organisers.all()) - edup.fix(new_event) - - messages.info( - request, - _("Creation of a merged event has been successfully completed."), - ) - return HttpResponseRedirect(new_event.get_absolute_url()) - - return render( - request, - "agenda_culturel/merge_duplicate.html", - context={"form": form, "object": edup}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] -) -def fix_duplicate(request, pk): - edup = get_object_or_404(DuplicatedEvents.objects.select_related(), pk=pk) - - if request.method == "POST": - form = FixDuplicates(request.POST, edup=edup) - - if form.is_valid(): - if form.is_action_no_duplicates(): - # all events are different - events = edup.get_duplicated() - - # get redirection date - if len(events) == 0: - date = None - else: - s_events = [e for e in events if not e.has_recurrences()] - if len(s_events) != 0: - s_event = s_events[0] - else: - s_event = events[0] - date = s_event.start_day - - messages.success(request, _("Events have been marked as unduplicated.")) - # delete the duplicated event (other_versions will be set to None on all events) - edup.delete() - if date is None: - return HttpResponseRedirect(reverse_lazy("home")) - else: - return HttpResponseRedirect( - reverse_lazy("day_view", args=[date.year, date.month, date.day]) - ) - - elif form.is_action_select(): - # one element has been selected to be the representative - selected = form.get_selected_event(edup) - if selected is None: - messages.error( - request, - _( - "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." - ), - ) - else: - edup.fix(selected) - messages.success( - request, - _("The selected event has been set as representative"), - ) - return HttpResponseRedirect(edup.get_absolute_url()) - elif form.is_action_remove(): - # one element is removed from the set - event = form.get_selected_event(edup) - if event is None: - messages.error( - request, - _( - "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." - ), - ) - return HttpResponseRedirect(edup.get_absolute_url()) - else: - event.other_versions = None - if edup.representative == event: - edup.representative = None - event.set_no_modification_date_changed() - event.save() - edup.save() - edup.events = [e for e in edup.events if e.pk != event.pk] - messages.success( - request, - _( - "The event has been withdrawn from the group and made independent." - ), - ) - if edup.nb_duplicated() == 1: - return HttpResponseRedirect(edup.get_absolute_url()) - else: - form = FixDuplicates(edup=edup) - elif form.is_action_update(): - # otherwise, an event will be updated using other elements - event = form.get_selected_event(edup) - if event is None: - messages.error( - request, - _( - "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." - ), - ) - return HttpResponseRedirect(edup.get_absolute_url()) - else: - return HttpResponseRedirect( - reverse_lazy("update_event", args=[edup.pk, event.pk]) - ) - else: - # otherwise, a new event will be created using a merging process - return HttpResponseRedirect( - reverse_lazy("merge_duplicate", args=[edup.pk]) - ) - else: - form = FixDuplicates(edup=edup) - - return render( - request, - "agenda_culturel/fix_duplicate.html", - context={"form": form, "object": edup}, - ) - - -class DuplicatedEventsUpdateView(LoginRequiredMixin, UpdateView): - model = DuplicatedEvents - fields = () - template_name = "agenda_culturel/fix_duplicate.html" - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_duplicatedevents") -def duplicates(request): - nb_removed = DuplicatedEvents.remove_singletons() - if nb_removed > 0: - messages.success( - request, - _("Cleaning up duplicates: {} item(s) fixed.").format(nb_removed), - ) - - filter = DuplicatedEventsFilter( - request.GET, queryset=DuplicatedEvents.objects.all().order_by("-pk") - ) - paginator = PaginatorFilter(filter, 10, request) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/duplicates.html", - { - "filter": filter, - "paginator_filter": response, - "paginator": paginator, - }, - ) - - -def set_duplicate(request, year, month, day, pk): - event = get_object_or_404(Event, pk=pk) - cday = CalendarDay(date(year, month, day)) - others = [ - e - for e in cday.get_events() - if e != event - and (event.other_versions is None or event.other_versions != e.other_versions) - and e.status != Event.STATUS.TRASH - ] - - form = SelectEventInList(events=others) - - if request.method == "POST": - form = SelectEventInList(request.POST, events=others) - if form.is_valid(): - selected = [o for o in others if o.pk == int(form.cleaned_data["event"])] - event.set_other_versions(selected) - # save them without updating modified date - event.set_no_modification_date_changed() - event.save() - if request.user.is_authenticated: - messages.success(request, _("The event was successfully duplicated.")) - return HttpResponseRedirect( - reverse_lazy("view_duplicate", args=[event.other_versions.pk]) - ) - else: - messages.info( - request, - _( - "The event has been successfully flagged as a duplicate. The moderation team will deal with your suggestion shortly." - ), - ) - return HttpResponseRedirect(event.get_absolute_url()) - - return render( - request, - "agenda_culturel/set_duplicate.html", - context={"form": form, "event": event}, - ) - - -######################### -## categorisation rules -######################### - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_categorisationrule") -def categorisation_rules(request): - paginator = Paginator( - CategorisationRule.objects.all() - .order_by("pk") - .select_related("category") - .select_related("place"), - 100, - ) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/categorisation_rules.html", - {"paginator_filter": response}, - ) - - -class CategorisationRuleCreateView( - LoginRequiredMixin, PermissionRequiredMixin, CreateView -): - model = CategorisationRule - permission_required = "agenda_culturel.add_categorisationrule" - success_url = reverse_lazy("categorisation_rules") - form_class = CategorisationRuleImportForm - - -class CategorisationRuleUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = CategorisationRule - permission_required = "agenda_culturel.change_categorisationrule" - form_class = CategorisationRuleImportForm - success_url = reverse_lazy("categorisation_rules") - success_message = _("The categorisation rule has been successfully modified.") - - -class CategorisationRuleDeleteView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - DeleteView, -): - model = CategorisationRule - permission_required = "agenda_culturel.delete_categorisationrule" - success_url = reverse_lazy("categorisation_rules") - success_message = _("The categorisation rule has been successfully deleted.") - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.apply_categorisationrules") -def apply_categorisation_rules(request): - if request.method == "POST": - form = CategorisationForm(request.POST) - if form.is_valid(): - nb = 0 - for epk, c in form.get_validated(): - e = Event.objects.get(pk=epk) - cat = Category.objects.filter(name=c).first() - e.category = cat - e.save() - nb += 1 - - if nb != 0: - if nb == 1: - messages.success( - request, - _( - "The rules were successfully applied and 1 event was categorised." - ), - ) - else: - messages.success( - request, - _( - "The rules were successfully applied and {} events were categorised." - ).format(nb), - ) - else: - messages.info( - request, - _( - "The rules were successfully applied and no events were categorised." - ), - ) - - return HttpResponseRedirect(reverse_lazy("categorisation_rules")) - else: - return render( - request, - "agenda_culturel/categorise_events_form.html", - context={"form": form}, - ) - else: - # first we check if events are not correctly categorised - to_categorise = [] - events = ( - Event.objects.filter(start_day__gte=datetime.now()) - .exclude(category=Category.get_default_category_id()) - .exclude(category=None) - .select_related("exact_location") - .select_related("category") - ) - for e in events: - c = CategorisationRule.get_category_from_rules(e) - if c and c != e.category: - to_categorise.append((e, c)) - - # then we apply rules on events without category - nb = 0 - to_save = [] - events = ( - Event.objects.filter(start_day__gte=datetime.now()) - .filter(Q(category=Category.get_default_category_id()) | Q(category=None)) - .select_related("exact_location") - .select_related("category") - ) - for e in events: - success = CategorisationRule.apply_rules(e) - if success: - nb += 1 - to_save.append(e) - - if nb != 0: - Event.objects.bulk_update(to_save, fields=["category"]) - - # set messages - if nb != 0: - if nb == 1: - messages.success( - request, - _( - "The rules were successfully applied and 1 event with default category was categorised." - ), - ) - else: - messages.success( - request, - _( - "The rules were successfully applied and {} events with default category were categorised." - ).format(nb), - ) - else: - messages.info( - request, - _( - "The rules were successfully applied and no events were categorised." - ), - ) - - if len(to_categorise) != 0: - form = CategorisationForm(events=to_categorise) - return render( - request, - "agenda_culturel/categorise_events_form.html", - context={ - "form": form, - "events": dict((e.pk, e) for e, c in to_categorise), - "categories": dict((e.pk, c) for e, c in to_categorise), - }, - ) - else: - return HttpResponseRedirect(reverse_lazy("categorisation_rules")) - - -######################### -## Places -######################### - - -class PlaceListView(ListView): - model = Place - ordering = ["name__unaccent"] - - -class PlaceListAdminView(PermissionRequiredMixin, ListView): - model = Place - paginate_by = 10 - permission_required = "agenda_culturel.add_place" - ordering = ["name__unaccent"] - template_name = "agenda_culturel/place_list_admin.html" - - -class PlaceDetailView(ListView): - model = Place - template_name = "agenda_culturel/place_detail.html" - paginate_by = 10 - - def get_queryset(self): - self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) - return ( - get_event_qs(self.request) - .filter(exact_location=self.place) - .filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - .filter(start_day__gte=datetime.now()) - .order_by("start_day") - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["object"] = self.place - return context - - -class PlaceDetailViewPast(PlaceDetailView): - def get_queryset(self): - self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) - self.past = True - return ( - get_event_qs(self.request) - .filter(exact_location=self.place) - .filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - .filter(start_day__lte=datetime.now()) - .order_by("-start_day") - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["past"] = self.past - return context - - -class UpdatePlaces: - def form_valid(self, form): - result = super().form_valid(form) - p = form.instance - - if not hasattr(self, "nb_applied"): - self.nb_applied = 0 - - # if required, find all matching events - if form.apply(): - self.nb_applied += p.associate_matching_events() - - if self.nb_applied > 1: - messages.success( - self.request, - _("{} events have been updated.").format(self.nb_applied), - ) - elif self.nb_applied == 1: - messages.success(self.request, _("1 event has been updated.")) - else: - messages.info(self.request, _("No events have been modified.")) - return result - - -class PlaceUpdateView( - UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, UpdateView -): - model = Place - permission_required = "agenda_culturel.change_place" - success_message = _("The place has been successfully updated.") - form_class = PlaceForm - - -class PlaceCreateView( - UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, CreateView -): - model = Place - permission_required = "agenda_culturel.add_place" - success_message = _("The place has been successfully created.") - form_class = PlaceForm - - -class PlaceDeleteView(PermissionRequiredMixin, DeleteView): - model = Place - permission_required = "agenda_culturel.delete_place" - success_url = reverse_lazy("view_places_admin") - - -class UnknownPlacesListView(PermissionRequiredMixin, ListView): - model = Event - permission_required = "agenda_culturel.add_place" - paginate_by = 10 - ordering = ["-pk"] - template_name = "agenda_culturel/place_unknown_list.html" - queryset = Event.get_qs_events_with_unkwnon_place() - - -def fix_unknown_places(request): - # get all places - places = Place.objects.all() - # get all events without exact location - u_events = Event.get_qs_events_with_unkwnon_place() - - to_be_updated = [] - # try to find matches - for ue in u_events: - for p in places: - if p.match(ue): - ue.exact_location = p - to_be_updated.append(ue) - continue - # update events with a location - Event.objects.bulk_update(to_be_updated, fields=["exact_location"]) - - # create a success message - nb = len(to_be_updated) - if nb > 1: - messages.success(request, _("{} events have been updated.").format(nb)) - elif nb == 1: - messages.success(request, _("1 event has been updated.")) - else: - messages.info(request, _("No events have been modified.")) - - # come back to the list of places - return HttpResponseRedirect(reverse_lazy("view_unknown_places")) - - -class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): - model = Event - permission_required = ( - "agenda_culturel.change_place", - "agenda_culturel.change_event", - ) - form_class = EventAddPlaceForm - template_name = "agenda_culturel/place_unknown_form.html" - - def form_valid(self, form): - self.modified_event = form.cleaned_data.get("place") - self.add_alias = form.cleaned_data.get("add_alias") - result = super().form_valid(form) - - if form.cleaned_data.get("place"): - messages.success( - self.request, - _("The selected place has been assigned to the event."), - ) - if form.cleaned_data.get("add_alias"): - messages.success( - self.request, - _("A new alias has been added to the selected place."), - ) - - nb_applied = form.cleaned_data.get("place").associate_matching_events() - - if nb_applied > 1: - messages.success( - self.request, - _("{} events have been updated.").format(nb_applied), - ) - elif nb_applied == 1: - messages.success(self.request, _("1 event has been updated.")) - else: - messages.info(self.request, _("No events have been modified.")) - - return result - - def get_success_url(self): - if self.modified_event: - return reverse_lazy("view_unknown_places") - else: - param = "?add=1" if self.add_alias else "" - return reverse_lazy("add_place_from_event", args=[self.object.pk]) + param - - -class PlaceFromEventCreateView(PlaceCreateView): - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["event"] = self.event - return context - - def get_initial(self, *args, **kwargs): - initial = super().get_initial(**kwargs) - 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): - result = super().form_valid(form) - self.event.exact_location = form.instance - self.event.save(update_fields=["exact_location"]) - return result - - def get_success_url(self): - return self.event.get_absolute_url() - - -######################### -## Organisations -######################### - - -class OrganisationListView(ListView): - model = Organisation - paginate_by = 10 - ordering = ["name__unaccent"] - - -class OrganisationDetailView(ListView): - model = Organisation - template_name = "agenda_culturel/organisation_detail.html" - paginate_by = 10 - - def get_queryset(self): - self.organisation = ( - Organisation.objects.filter(pk=self.kwargs["pk"]) - .prefetch_related("organised_events") - .first() - ) - return ( - get_event_qs(self.request) - .filter(organisers__in=[self.kwargs["pk"]]) - .filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - .filter(start_day__gte=datetime.now()) - .order_by("start_day") - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["object"] = self.organisation - return context - - -class OrganisationDetailViewPast(OrganisationDetailView): - def get_queryset(self): - self.organisation = ( - Organisation.objects.filter(pk=self.kwargs["pk"]) - .prefetch_related("organised_events") - .first() - ) - self.past = True - return ( - get_event_qs(self.request) - .filter(organisers__in=[self.kwargs["pk"]]) - .filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - .filter(start_day__lte=datetime.now()) - .order_by("-start_day") - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["past"] = self.past - return context - - -class OrganisationUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): - model = Organisation - permission_required = "agenda_culturel.change_organisation" - success_message = _("The organisation has been successfully updated.") - fields = "__all__" - - -class OrganisationCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): - model = Organisation - permission_required = "agenda_culturel.add_organisation" - success_message = _("The organisation has been successfully created.") - fields = "__all__" - - -class OrganisationDeleteView(PermissionRequiredMixin, DeleteView): - model = Organisation - permission_required = "agenda_culturel.delete_organisation" - success_url = reverse_lazy("view_organisations") - - -######################### -## Tags -######################### - - -class TagUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): - model = Tag - permission_required = "agenda_culturel.change_tag" - form_class = TagForm - success_message = _("The tag has been successfully updated.") - - -class TagCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): - model = Tag - permission_required = "agenda_culturel.add_tag" - form_class = TagForm - success_message = _("The tag has been successfully created.") - - def get_initial(self, *args, **kwargs): - initial = super().get_initial(**kwargs) - if "name" in self.request.GET: - initial["name"] = self.request.GET.get("name") - return initial - - def form_valid(self, form): - Tag.clear_cache() - return super().form_valid(form) - - -class TagDeleteView(PermissionRequiredMixin, DeleteView): - model = Tag - permission_required = "agenda_culturel.delete_tag" - success_url = reverse_lazy("view_all_tags") - - -def view_tag_past(request, t): - return view_tag(request, t, True) - - -def view_tag(request, t, past=False): - now = date.today() - - qs = get_event_qs(request).filter(tags__contains=[t]) - - if past: - qs = qs.filter(start_day__lt=now).order_by("-start_day", "-start_time") - else: - qs = qs.filter(start_day__gte=now).order_by("start_day", "start_time") - - qs = qs.filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - - paginator = Paginator(qs, 10) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - 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, - "past": past, - } - return render(request, "agenda_culturel/tag.html", context) - - -def statistics(request, pk=None): - if pk is not None: - rimport = RecurrentImport.objects.filter(pk=pk) - source = rimport.values("source").first()["source"] - qs = Event.objects.filter(import_sources__contains=[source]) - else: - rimport = None - qs = Event.objects - - stats = {} - stats_months = {} - first = {} - last = {} - - ev_published = qs.filter( - Q(status=Event.STATUS.PUBLISHED) - & ( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - ) - - for v in ["start_day", "created_date__date"]: - after = 24 - last[v] = ( - date.today() - if v == "created_date__date" - else date.today() + timedelta(weeks=after) - ) - last[v] = last[v].replace( - day=_calendar.monthrange(last[v].year, last[v].month)[1] - ) - - r = 8 * 30 - if v == "start_day": - r += after * 7 - first[v] = (last[v] - timedelta(days=r)).replace(day=1) - - ev_days = ev_published.annotate(day=F(v)).filter( - Q(day__lte=last[v]) & Q(day__gte=first[v]) - ) - - stats[v] = ev_days.values("day").annotate(total=Count("day")).order_by("day") - - stats_months[v] = ( - ev_days.annotate(month=TruncMonth("day")) - .values("month") - .annotate(total=Count("month")) - .order_by("month") - ) - - nb_by_city = ( - ev_published.annotate(city=F("exact_location__city")) - .filter(city__isnull=False) - .values("city") - .annotate(total=Count("city")) - .order_by("-total") - ) - - limit = datetime.now() + timedelta(days=-30) - - stat_qs = qs.filter(start_day__gte=F("created_date")).annotate( - foresight=ExtractDay(F("start_day") - F("created_date")) - ) - - statsa = stat_qs.filter().aggregate( - minimum=Min("foresight"), - maximum=Max("foresight"), - mean=Avg("foresight"), - median=Median("foresight"), - stdev=StdDev("foresight"), - ) - - statsm = stat_qs.filter(created_date__gte=limit).aggregate( - minimum=Min("foresight"), - maximum=Max("foresight"), - mean=Avg("foresight"), - median=Median("foresight"), - stdev=StdDev("foresight"), - ) - - stats_foresight = [ - [ - _(x), - round(statsa[x], 2) if statsa[x] is not None else "-", - round(statsm[x], 2) if statsm[x] is not None else "-", - ] - for x in statsa - ] - - context = { - "stats_by_startday": stats["start_day"], - "stats_by_creation": stats["created_date__date"], - "stats_months_by_startday": stats_months["start_day"], - "stats_months_by_creation": stats_months["created_date__date"], - "first_by_startday": first["start_day"], - "last_by_startday": last["start_day"], - "first_by_creation": first["created_date__date"], - "last_by_creation": last["created_date__date"], - "nb_by_city": nb_by_city, - "stats_foresight": stats_foresight, - "object": rimport.first() if rimport else None, - } - - if pk is None: - return render(request, "agenda_culturel/statistics.html", context) - else: - return render(request, "agenda_culturel/rimport-statistics.html", context) - - -def tag_list(request): - tags = Event.get_all_tags() - r_tags = [t["tag"] for t in tags] - objects = Tag.objects.order_by("name").all() - d_objects = dict() - for o in objects: - d_objects[o.name] = o - - tags = [ - t | {"obj": d_objects[t["tag"]]} if t["tag"] in d_objects else t for t in tags - ] - tags += [ - {"obj": o, "tag": o.name, "count": 0} for o in objects if o.name not in r_tags - ] - - context = { - "tags": sorted( - tags, - key=lambda x: emoji.demojize( - remove_accents(x["tag"]).lower(), delimiters=("000", "") - ), - ) - } - return render(request, "agenda_culturel/tags.html", context) - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.change_tag") -def rename_tag(request, t): - form = TagRenameForm(name=t) - - if request.method == "POST": - form = TagRenameForm(request.POST, name=t) - if form.is_valid(): - save = True - if form.cleaned_data["name"] == t: - messages.warning( - request, - _("You have not modified the tag name."), - ) - save = False - elif not form.is_force(): - if ( - Event.objects.filter( - tags__contains=[form.cleaned_data["name"]] - ).count() - > 0 - ): - if Tag.objects.filter(name=form.cleaned_data["name"]): - messages.warning( - request, - ( - _( - "This tag {} is already in use, and is described by different information from the current tag. You can force renaming by checking the corresponding option. The information associated with tag {} will be deleted, and all events associated with tag {} will be associated with tag {}." - ) - ).format( - form.cleaned_data["name"], - t, - t, - form.cleaned_data["name"], - ), - ) - else: - messages.warning( - request, - ( - _( - "This tag {} is already in use. You can force renaming by checking the corresponding option." - ) - ).format(form.cleaned_data["name"]), - ) - save = False - form = TagRenameForm(request.POST, name=t, force=True) - - if save: - # find all matching events and update them - events = Event.objects.filter(tags__contains=[t]) - new_name = form.cleaned_data["name"] - for e in events: - e.tags = [te for te in e.tags if te != t] - if new_name not in e.tags: - 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 new_name not 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, - (_("The tag {} has been successfully renamed to {}.")).format( - t, form.cleaned_data["name"] - ), - ) - return HttpResponseRedirect( - reverse_lazy("view_tag", kwargs={"t": form.cleaned_data["name"]}) - ) - - nb = Event.objects.filter(tags__contains=[t]).count() - - return render( - request, - "agenda_culturel/tag_rename_form.html", - context={"form": form, "tag": t, "nb": nb}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.delete_tag") -def delete_tag(request, t): - respage = reverse_lazy("view_all_tags") - - if request.method == "POST": - # remove tag from events - events = Event.objects.filter(tags__contains=[t]) - for e in events: - 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: - tag_object.delete() - - messages.success( - request, - (_("The tag {} has been successfully deleted.")).format(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, - }, - ) - - -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", - ) - - -class UserProfileUpdateView( - SuccessMessageMixin, - LoginRequiredMixin, - UpdateView, -): - model = UserProfile - success_message = _("Your user profile has been successfully modified.") - success_url = reverse_lazy("administration") - form_class = UserProfileForm - - def get_object(self): - return self.request.user.userprofile From 9a99ec42ef9688def8ad56a5a6f262d5a6037603 Mon Sep 17 00:00:00 2001 From: SebF Date: Sun, 23 Mar 2025 13:19:49 +0100 Subject: [PATCH 02/20] Changement des views en module avec fichier historique --- src/agenda_culturel/views/__init__.py | 1 + src/agenda_culturel/views/oldviews.py | 3076 +++++++++++++++++++++++++ 2 files changed, 3077 insertions(+) create mode 100644 src/agenda_culturel/views/__init__.py create mode 100644 src/agenda_culturel/views/oldviews.py diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py new file mode 100644 index 0000000..00e8edd --- /dev/null +++ b/src/agenda_culturel/views/__init__.py @@ -0,0 +1 @@ +from .oldviews import * diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py new file mode 100644 index 0000000..72d5f15 --- /dev/null +++ b/src/agenda_culturel/views/oldviews.py @@ -0,0 +1,3076 @@ +import calendar as _calendar +import hashlib +import logging +from datetime import date, timedelta + +import emoji +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import ( + LoginRequiredMixin, + PermissionRequiredMixin, + UserPassesTestMixin, +) +from django.contrib.messages.views import SuccessMessageMixin +from django.core.cache import cache +from django.core.mail import mail_admins +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.db.models import Count, F, Func, OuterRef, Q, Subquery +from django.db.models.functions import TruncMonth +from django.http import ( + Http404, + HttpResponse, + HttpResponseForbidden, + HttpResponseRedirect, +) +from django.shortcuts import get_object_or_404, render +from django.urls import reverse, reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.timezone import datetime +from django.utils.translation import gettext_lazy as _ +from django.views.generic import DetailView, ListView +from django.views.generic.edit import ( + CreateView, + DeleteView, + ModelFormMixin, + UpdateView, +) +from honeypot.decorators import check_honeypot +from django.db.models.aggregates import StdDev +from django.db.models import Avg, Max, Min +from django.db.models import Aggregate, FloatField +from django.db.models.functions import ExtractDay + +from ..calendar import CalendarDay, CalendarList, CalendarMonth, CalendarWeek +from ..celery import app as celery_app +from ..celery import ( + import_events_from_json, + import_events_from_url, + import_events_from_urls, + run_all_recurrent_imports, + run_all_recurrent_imports_canceled, + run_all_recurrent_imports_failed, + run_recurrent_import, + update_orphan_pure_import_events, +) +from ..filters import ( + DuplicatedEventsFilter, + EventFilter, + EventFilterAdmin, + MessagesFilterAdmin, + RecurrentImportFilter, + SearchEventFilter, + SimpleSearchEventFilter, +) +from ..forms import ( + BatchImportationForm, + CategorisationForm, + CategorisationRuleImportForm, + EventAddPlaceForm, + EventForm, + EventFormWithContact, + EventModerateForm, + FixDuplicates, + MergeDuplicates, + MessageEventForm, + MessageForm, + PlaceForm, + RecurrentImportForm, + SelectEventInList, + SimpleContactForm, + TagForm, + TagRenameForm, + URLSubmissionFormSet, + URLSubmissionFormWithContact, + UserProfileForm, +) +from ..import_tasks.extractor import Extractor +from ..models import ( + BatchImportation, + CategorisationRule, + Category, + DuplicatedEvents, + Event, + Message, + Organisation, + Place, + RecurrentImport, + StaticContent, + Tag, + remove_accents, + UserProfile, +) +from ..utils import PlaceGuesser + +logger = logging.getLogger(__name__) + + +class Median(Aggregate): + function = "PERCENTILE_CONT" + name = "median" + output_field = FloatField() + template = "%(function)s(0.5) WITHIN GROUP (ORDER BY %(expressions)s)" + + +class PaginatorFilter(Paginator): + def __init__(self, filter, nb, request): + self.request = request + self.filter = filter + + super().__init__(filter.qs, nb) + + self.url_first_page = PaginatorFilter.update_param( + self.request.get_full_path(), "page", 1 + ) + self.url_last_page = PaginatorFilter.update_param( + self.request.get_full_path(), "page", self.num_pages + ) + + def update_param(params, key, value): + p = params.split("?") + root = p[0] + if len(p) > 1: + other = p[1] + others = other.split("&") + others = [o for o in others if not o.startswith(key)] + others += [key + "=" + str(value)] + return root + "?" + "&".join(others) + else: + return root + "?" + key + "=" + str(value) + + def page(self, *args, **kwargs): + page = super().page(*args, **kwargs) + + try: + page.url_previous_page = PaginatorFilter.update_param( + self.request.get_full_path(), + "page", + page.previous_page_number(), + ) + except EmptyPage: + page.url_previous_page = self.request.get_full_path() + + try: + page.url_next_page = PaginatorFilter.update_param( + self.request.get_full_path(), "page", page.next_page_number() + ) + except EmptyPage: + page.url_next_page = self.request.get_full_path() + + return page + + +# +# +# Useful for translation +to_be_translated = [ + _("Recurrent import name"), + _("Add another"), + _("Browse..."), + _("No file selected."), +] + + +def get_event_qs(request): + if request.user.is_authenticated: + return Event.objects.filter() + else: + return Event.objects.filter(status=Event.STATUS.PUBLISHED) + + +def page_not_found(request, exception=None): + return render(request, "page-erreur.html", status=404, context={"error": 404}) + + +def internal_server_error(request): + try: + mail_admins( + request.site.name + _(": error 500"), + _("An internal error has occurred on site {} at address {}.").format( + request.site.name, request.build_absolute_uri() + ), + ) + except Exception: + pass + return render(request, "page-erreur.html", status=500, context={"error": 500}) + + +def thank_you(request): + return render(request, "agenda_culturel/thank_you.html") + + +def mentions_legales(request): + context = { + "title": "Mentions légales", + "static_content": "mentions_legales", + "url_path": reverse_lazy("mentions_legales"), + } + return render(request, "agenda_culturel/page-single.html", context) + + +def about(request): + rimports = ( + RecurrentImport.objects.filter(~Q(recurrence=RecurrentImport.RECURRENCE.NEVER)) + .order_by("name__unaccent") + .all() + ) + context = { + "title": "À propos", + "static_content": "about", + "url_path": reverse_lazy("about"), + "rimports": rimports, + } + return render(request, "agenda_culturel/page-rimports-list.html", context) + + +def moderation_rules(request): + context = { + "title": _("Moderation rules"), + "static_content": "moderation_rules", + "url_path": reverse_lazy("moderation_rules"), + } + return render(request, "agenda_culturel/page-single.html", context) + + +def import_requirements(request): + context = { + "title": _("Import requirements"), + "static_content": "import_requirements", + "url_path": reverse_lazy("import_requirements"), + } + return render(request, "agenda_culturel/page-single.html", context) + + +def home(request, cat=None): + return week_view(request, home=True, cat=cat) + + +def month_view(request, year=None, month=None, cat=None): + now = date.today() + if year is None and month is None: + day = now.day + else: + day = None + if year is None: + year = now.year + if month is None: + month = now.month + + request = EventFilter.set_default_values(request) + qs = get_event_qs(request).only( + "title", + "start_day", + "start_time", + "category", + "other_versions", + "recurrences", + "end_day", + "end_time", + "uuids", + "status", + "tags", + ) + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + + filter = EventFilter(request.GET, qs, request=request) + + if filter.has_category_parameters(): + return HttpResponseRedirect(filter.get_new_url()) + + cmonth = CalendarMonth(year, month, filter, day=day) + + context = { + "calendar": cmonth, + "this_month": day is not None, + "filter": filter, + "category": category, + "init_date": now if cmonth.today_in_calendar() else cmonth.firstdate, + } + return render(request, "agenda_culturel/page-month.html", context) + + +def week_view(request, year=None, week=None, home=False, cat=None): + now = date.today() + if year is None: + year = now.isocalendar()[0] + if week is None: + week = now.isocalendar()[1] + + request = EventFilter.set_default_values(request) + qs = ( + get_event_qs(request) + .select_related("exact_location") + .only( + "title", + "start_day", + "start_time", + "category", + "other_versions", + "recurrences", + "end_day", + "end_time", + "uuids", + "status", + "tags", + "local_image", + "image", + "image_alt", + "exact_location", + "description", + ) + ) + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + filter = EventFilter(request.GET, qs, request=request) + + if filter.has_category_parameters(): + return HttpResponseRedirect(filter.get_new_url()) + + cweek = CalendarWeek(year, week, filter) + + context = { + "year": year, + "week": week, + "calendar": cweek, + "filter": filter, + "category": category, + "init_date": now if cweek.today_in_calendar() else cweek.firstdate, + } + if home: + context["home"] = 1 + return render(request, "agenda_culturel/page-week.html", context) + + +def day_view(request, year=None, month=None, day=None, cat=None): + if year is None or month is None or day is None: + if "when" in request.POST: + when = datetime.strptime(request.POST["when"], "%Y-%m-%d") + year = when.year + month = when.month + day = when.day + + request = EventFilter.set_default_values(request) + qs = get_event_qs(request).select_related("exact_location") + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + filter = EventFilter(request.GET, qs, request=request) + + return HttpResponseRedirect( + reverse_lazy("day_view", args=[year, month, day]) + + "?" + + filter.get_url() + ) + + return upcoming_events(request, year, month, day, 0, cat) + + +def upcoming_events(request, year=None, month=None, day=None, neighsize=1, cat=None): + now = date.today() + if year is None: + year = now.year + if month is None: + month = now.month + if day is None: + day = now.day + + day = date(year, month, day) + day = day + timedelta(days=neighsize) + + request = EventFilter.set_default_values(request) + qs = get_event_qs(request).select_related("exact_location") + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + + filter = EventFilter(request.GET, qs, request=request) + + if filter.has_category_parameters(): + return HttpResponseRedirect(filter.get_new_url()) + + cal = CalendarList( + day + timedelta(days=-neighsize), + day + timedelta(days=neighsize), + filter, + True, + ) + + context = { + "calendar": cal, + "now": now, + "day": day, + "init_date": now if cal.today_in_calendar() else day, + "filter": filter, + "date_pred": day + timedelta(days=-neighsize - 1), + "date_next": day + timedelta(days=neighsize + 1), + "category": category, + } + + return render(request, "agenda_culturel/page-upcoming.html", context) + + +class StaticContentCreateView(LoginRequiredMixin, CreateView): + model = StaticContent + fields = ["text"] + permission_required = "agenda_culturel.add_staticcontent" + + def form_valid(self, form): + form.instance.name = self.request.GET["name"] + form.instance.url_path = self.request.GET["url_path"] + return super().form_valid(form) + + +class StaticContentUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = StaticContent + permission_required = "agenda_culturel.change_staticcontent" + fields = ["text"] + success_message = _("The static content has been successfully updated.") + + +def update_from_source(request, pk): + event = get_object_or_404(Event, pk=pk) + + url = event.get_updateable_uuid() + 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, + ) + messages.success( + request, + _("The event update has been queued and will be completed shortly."), + ) + + return HttpResponseRedirect(event.get_absolute_url()) + + +class EventUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = Event + permission_required = "agenda_culturel.change_event" + form_class = EventForm + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["is_authenticated"] = self.request.user.is_authenticated + kwargs["is_moderation_expert"] = ( + self.request.user.userprofile.is_moderation_expert + ) + kwargs["is_cloning"] = self.is_cloning + kwargs["is_edit_from_moderation"] = self.is_edit_force() + 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") and self.with_msg + else "" + ) + return mark_safe(_("The event has been successfully modified.") + txt) + + def is_edit_force(self): + return "edit-force" in self.request.path.split("/") + + def get_object(self, queryset=None): + event = super().get_object(queryset) + if event.status == Event.STATUS.DRAFT: + event.status = Event.STATUS.PUBLISHED + if self.is_edit_force(): + event.free_modification_lock(self.request.user) + return event + + 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("/") + if self.is_cloning: + messages.info( + self.request, + _( + "Changes will be visible on a local copy of the event. The version identical to the imported source will be hidden." + ), + ) + self.is_simple_cloning = "simple-clone" in self.request.path.split("/") + result = super().get_initial() + + if self.is_cloning and "other_versions" not in result: + obj = self.get_object() + # if no DuplicatedEvents is associated, create one + obj.other_versions = DuplicatedEvents.objects.create() + obj.other_versions.save() + # save them without updating modified date + obj.set_no_modification_date_changed() + 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 + result["simple_cloning"] = True + + if self.is_cloning or self.is_simple_cloning: + obj = self.get_object() + if obj.local_image: + result["old_local_image"] = obj.local_image.name + + return result + + def get_success_url(self): + if "save_and_next" in self.request.POST: + return reverse_lazy("moderate_event_next", args=[self.object.pk]) + else: + return self.object.get_absolute_url() + + +class EventModerateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = Event + permission_required = "agenda_culturel.change_event" + template_name = "agenda_culturel/event_form_moderate.html" + form_class = EventModerateForm + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["is_moderation_expert"] = ( + self.request.user.userprofile.is_moderation_expert + ) + 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") and self.with_msg + else "" + ) + return mark_safe( + _('The event {} has been moderated with success.').format( + self.object.get_absolute_url(), self.object.title + ) + + txt + ) + + def is_moderate_next(self): + return "after" in self.request.path.split("/") + + def is_moderate_back(self): + return "back" in self.request.path.split("/") + + def is_moderate_force(self): + return "moderate-force" in self.request.path.split("/") + + def is_starting_moderation(self): + return "pk" not 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, opk): + # 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)) + ) + ) + else: + qs = qs.filter(Q(start_day__gte=start_day) & ~Q(pk=opk)) + + # get only possibly representative events + qs = qs.filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + + # remove trash events + qs = qs.filter(~Q(status=Event.STATUS.TRASH)) + + # sort by datetime + qs = qs.order_by("start_day", "start_time") + + return qs.first() + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.is_moderate_next(): + context["pred"] = self.kwargs["pred"] + return context + + def get_object(self, queryset=None): + if self.is_starting_moderation(): + now = datetime.now() + event = EventModerateView.get_next_event(now.date(), now.time(), None) + else: + event = super().get_object(queryset) + if event.status == Event.STATUS.DRAFT: + event.status = Event.STATUS.PUBLISHED + if self.is_moderate_back(): + pred = Event.objects.filter(pk=self.kwargs["pred"]).first() + pred.free_modification_lock(self.request.user) + if self.is_moderate_force(): + event.free_modification_lock(self.request.user, False) + return event + + def post(self, request, *args, **kwargs): + try: + return super().post(request, args, kwargs) + except Http404: + return HttpResponseRedirect( + reverse_lazy("error_next_event", args=[self.object.pk]) + ) + + 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_edit_local" in self.request.POST: + return reverse_lazy("edit_event", args=[self.object.get_local_version().pk]) + else: + return self.object.get_absolute_url() + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.change_event") +def error_next_event(request, pk): + obj = Event.objects.filter(pk=pk).first() + + return render( + request, + "agenda_culturel/event_next_error_message.html", + {"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() + # free lock + obj.free_modification_lock(request.user) + start_day = obj.start_day + start_time = obj.start_time + + next_obj = EventModerateView.get_next_event(start_day, start_time, pk) + 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, None) + return HttpResponseRedirect(reverse_lazy("moderate_event", args=[obj.pk])) + + +class EventDeleteView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + DeleteView, +): + model = Event + permission_required = "agenda_culturel.delete_event" + success_url = reverse_lazy("recent") + success_message = _("The event has been successfully deleted.") + + +class EventDetailView(UserPassesTestMixin, DetailView, ModelFormMixin): + model = Event + form_class = MessageEventForm + template_name = "agenda_culturel/page-event.html" + queryset = ( + Event.objects.select_related("exact_location") + .select_related("category") + .select_related("other_versions") + .select_related("other_versions__representative") + .prefetch_related("message_set") + ) + + def test_func(self): + return ( + self.request.user.is_authenticated + or self.get_object().status == Event.STATUS.PUBLISHED + ) + + def get_object(self): + o = super().get_object() + o.download_missing_image() + if "year" in self.kwargs: + y = self.kwargs["year"] + m = self.kwargs["month"] + d = self.kwargs["day"] + obj = o.get_recurrence_at_date(y, m, d) + obj.set_current_date(date(y, m, d)) + return obj + else: + return o + + 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/") +@permission_required("agenda_culturel.change_event") +def change_status_event(request, pk, status): + event = get_object_or_404(Event, pk=pk) + + if request.method == "POST": + event.status = Event.STATUS(status) + fields = ["status", "moderated_date", "moderated_by_user"] + event.set_in_moderation_process() + event.update_modification_dates() + event.save(update_fields=fields) + 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.")) + + return HttpResponseRedirect(event.get_absolute_url()) + + else: + cancel_url = request.META.get("HTTP_REFERER", "") + if cancel_url == "": + cancel_url = reverse_lazy("home") + return render( + request, + "agenda_culturel/event_confirm_change_status.html", + {"status": status, "event": event, "cancel_url": cancel_url}, + ) + + +def import_event_proxy(request): + return render(request, "agenda_culturel/event_import.html") + + +class EventCreateView(SuccessMessageMixin, CreateView): + model = Event + form_class = EventFormWithContact + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["is_authenticated"] = self.request.user.is_authenticated + return kwargs + + def get_success_url(self): + if self.request.user.is_authenticated: + if "save_and_next" in self.request.POST: + return reverse_lazy("moderate_event_next", args=[self.object.pk]) + else: + return self.object.get_absolute_url() + else: + return reverse_lazy("home") + + def get_success_message(self, cleaned_data): + if self.request.user.is_authenticated: + return mark_safe( + _('The event was created: {}.').format( + self.object.get_absolute_url(), self.object.title + ) + ) + else: + return _( + "The event has been submitted and will be published as soon as it has been validated by the moderation team." + ) + + def form_valid(self, form): + 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 = form.cleaned_data.get("comments") not in ["", None] + form.instance.add_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 + + +# A class to evaluate the URL according to the existing events and the authentification +# level of the user +class URLEventEvaluation: + def __init__(self, form, is_authenticated): + self.form = form + self.is_authenticated = is_authenticated + + self.cat = None + self.tags = [] + self.existing = None + self.url = form.cleaned_data.get("url") + self.event = None + if self.url is not None: + self.url = Extractor.clean_url(self.url) + # we check if the url is known + self.existing = Event.objects.filter(uuids__contains=[self.url]) + # if it's unknown + if len(self.existing) == 0: + self.existing = None + self.cat = form.cleaned_data.get("category") + if self.cat is not None: + self.cat = self.cat.name + self.tags = form.cleaned_data.get("tags") + + else: + published = [ + e for e in self.existing if e.status == Event.STATUS.PUBLISHED + ] + + if self.is_authenticated or len(published) > 1: + self.event = ( + published[0] if len(published) > 1 else self.existing[0] + ) + else: + self.event = None + + def exists(self): + return self.url is not None + + def is_new(self): + return self.exists() and self.existing is None + + def is_event_visible(self): + return self.event is not None + + def get_event(self): + if self.event is None: + return None + else: + return self.event + + def get_link(self): + e = self.get_event() + if e is None: + return "" + else: + return '' + escape(e.title) + "" + + def to_list(self): + if self.is_new(): + return (self.url, self.cat, self.tags) + + +def import_from_urls(request): + 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() + ): + # evaluate all the forms + ucat = [ + URLEventEvaluation(form, request.user.is_authenticated) + for form in formset.forms + ] + + # for each not new, add a message + for uc in ucat: + if uc.exists() and not uc.is_new(): + if uc.is_event_visible(): + messages.info( + request, + mark_safe( + _( + "{} has not been submitted since it" + "s already known: {}." + ).format(uc.url, uc.get_link()) + ), + ) + else: + messages.info( + request, + _( + "{} has not been submitted since it" + "s already known and currently into moderation process." + ).format(uc.url), + ) + + # keep only new ones + ucat = [uc.to_list() for uc in ucat if uc.is_new()] + + # finally process them or go back to home page + if len(ucat) > 0: + messages.info( + 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, + ) + 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) + + +def import_from_url(request): + form = URLSubmissionFormWithContact(is_authenticated=request.user.is_authenticated) + + initial = { + "start_day": date.today() + timedelta(days=1), + "start_time": "20:00", + "end_time": "22:00", + } + + form_event = EventForm(initial=initial) + + # if the form has been sent + if request.method == "POST": + form = URLSubmissionFormWithContact( + request.POST, is_authenticated=request.user.is_authenticated + ) + + # if the form is valid + if form.is_valid(): + uc = URLEventEvaluation(form, request.user.is_authenticated) + + if uc.exists() and not uc.is_new(): + if uc.is_event_visible(): + messages.info( + request, + mark_safe( + _( + "{} has not been submitted since its already known: {}." + ).format(uc.url, uc.get_link()) + ), + ) + return HttpResponseRedirect(uc.get_event().get_absolute_url()) + else: + messages.info( + request, + _( + "{} has not been submitted since it" + "s already known and currently into moderation process." + ).format(uc.url), + ) + return HttpResponseRedirect(reverse("home")) + + else: + messages.info( + 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"), + ) + return HttpResponseRedirect(reverse("thank_you")) + + return render( + request, + "agenda_culturel/import.html", + context={"form": form, "form_event": form_event}, + ) + + +def export_event_ical(request, year, month, day, pk): + event = get_object_or_404(Event, pk=pk) + event = event.get_recurrence_at_date(year, month, day) + + events = list() + events.append(event) + + cal = Event.export_to_ics(events, request) + + response = HttpResponse(content_type="text/calendar") + response.content = cal.to_ical().decode("utf-8").replace("\r\n", "\n") + response["Content-Disposition"] = "attachment; filename={0}{1}".format( + event.title.replace("\n", " ").replace("\r", "")[0:32], ".ics" + ) + + return response + + +def export_ical(request, cat=None, tag=None, organisation_pk=None, place_pk=None): + now = date.today() + + qs = get_event_qs(request) + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + + if place_pk is not None: + qs = qs.filter(exact_location=place_pk) + if organisation_pk is not None: + qs = qs.filter(organisers__in=[organisation_pk]) + if tag is not None: + qs = qs.filter(tags__in=[tag]) + + request = EventFilter.set_default_values(request) + filter = EventFilter(request.GET, queryset=qs, request=request) + + if filter.has_category_parameters(): + return HttpResponseRedirect(filter.get_new_url()) + + id_cache = hashlib.md5( + ( + filter.get_url() + + "-" + + str(tag) + + "-" + + str(cat) + + "-" + + str(organisation_pk) + + "-" + + str(place_pk) + ).encode("utf8") + ).hexdigest() + ical = cache.get(id_cache) + if not ical: + calendar = CalendarList( + now + timedelta(days=-7), now + timedelta(days=+60), filter + ) + ical = calendar.export_to_ics(request) + cache.set(id_cache, ical, 3600) # 1 heure + + response = HttpResponse(content_type="text/calendar") + response.content = ical.to_ical().decode("utf-8").replace("\r\n", "\n") + extra = filter.to_str(" ") + if extra is None: + extra = "" + if category is not None: + extra += " " + str(category) + if place_pk is not None: + extra += ( + " @ " + Place.objects.filter(pk=place_pk).values("name").first()["name"] + ) + if organisation_pk is not None: + extra += ( + " - " + + Organisation.objects.filter(pk=organisation_pk) + .values("name") + .first()["name"] + ) + if tag is not None: + extra += " - " + emoji.replace_emoji(tag, replace="") + + response["Content-Disposition"] = "attachment; filename={0}{1}{2}".format( + "Pommes de lune", extra, ".ics" + ) + + 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 + + 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 + success_message = _("The contact message has been successfully deleted.") + success_url = reverse_lazy("messages") + + +class MessageUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = Message + permission_required = "agenda_culturel.change_message" + template_name = "agenda_culturel/message_moderation_form.html" + fields = ("spam", "closed", "comments") + + success_message = _( + "The contact message properties has been successfully modified." + ) + + success_url = reverse_lazy("messages") + + def get_form_kwargs(self): + """Return the keyword arguments for instantiating the form.""" + kwargs = super().get_form_kwargs() + if hasattr(self, "object"): + kwargs.update({"instance": self.object}) + return kwargs + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_event") +def activite(request): + now = date.today() + + days = [now] + while len(days) < 7 or days[-1].weekday() != 0: + days.append(days[-1] + timedelta(days=-1)) + + weeks = [days[-1]] + for w in range(0, 8): + weeks.append(weeks[-1] + timedelta(days=-7)) + + daily_modifications = Event.get_count_modifications([(d, 1) for d in days]) + weekly_modifications = Event.get_count_modifications([(w, 7) for w in weeks]) + + return render( + request, + "agenda_culturel/page-activity.html", + { + "daily_modifications": daily_modifications, + "weekly_modifications": weekly_modifications, + }, + ) + + +@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() + + # get information about recent modifications + days = [today] + for i in range(0, 2): + days.append(days[-1] + timedelta(days=-1)) + daily_modifications = Event.get_count_modifications([(d, 1) for d in days]) + + # get last created events + events = ( + Event.objects.all() + .order_by("-created_date") + .select_related("exact_location", "category")[:5] + ) + + # get last batch imports + rel_event = Event.objects.filter( + import_sources__contains=[OuterRef("url_source")] + ).values("pk")[:1] + batch_imports = ( + BatchImportation.objects.all() + .select_related("recurrentImport") + .annotate(event_id=Subquery(rel_event)) + .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]) + ) + + nb_failed = imported_events.filter( + last_run_status=BatchImportation.STATUS.FAILED + ).count() + nb_canceled = imported_events.filter( + last_run_status=BatchImportation.STATUS.CANCELED + ).count() + nb_running = imported_events.filter( + last_run_status=BatchImportation.STATUS.RUNNING + ).count() + nb_all = imported_events.count() + + # get some info about imported (or not) events + srcs = RecurrentImport.objects.all().values_list("source") + in_future = Event.objects.filter(Q(start_day__gte=today)) + nb_in_rimport = in_future.filter(Q(import_sources__overlap=srcs)).count() + nb_in_orphan_import = in_future.filter( + ( + Q(import_sources__isnull=False) + & (Q(modified_date__isnull=True) | Q(modified_date__lte=F("imported_date"))) + ) + & ~Q(import_sources__overlap=srcs) + ).count() + + # get all non moderated events + nb_not_moderated = Event.get_nb_not_moderated(today, nb_mod_days, nb_classes) + + 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_in_rimport": nb_in_rimport, + "nb_in_orphan_import": nb_in_orphan_import, + }, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_event") +def recent(request): + filter = EventFilterAdmin( + request.GET, queryset=Event.objects.all().order_by("-created_date") + ) + paginator = PaginatorFilter(filter, 10, request) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/recent.html", + {"filter": filter, "paginator_filter": response}, + ) + + +@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") + ) + paginator = PaginatorFilter(filter, 10, request) + page = request.GET.get("page") + + nb_spams = Message.objects.filter(spam=True).count() + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/messages.html", + {"filter": filter, "nb_spams": nb_spams, "paginator_filter": response}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_message") +def delete_cm_spam(request): + if request.method == "POST": + Message.objects.filter(spam=True).delete() + + messages.success(request, _("Spam has been successfully deleted.")) + return HttpResponseRedirect(reverse_lazy("messages")) + else: + nb_msgs = Message.objects.values("spam").annotate(total=Count("spam")) + nb_total = sum([nb["total"] for nb in nb_msgs]) + nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]]) + cancel_url = reverse_lazy("messages") + return render( + request, + "agenda_culturel/delete_spams_confirm.html", + { + "nb_total": nb_total, + "nb_spams": nb_spams, + "cancel_url": cancel_url, + }, + ) + + +def event_search(request, full=False): + categories = None + tags = None + places = None + organisations = None + rimports = None + + qs = get_event_qs(request).order_by("-start_day") + if not request.user.is_authenticated: + qs = qs.filter( + ( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + ) + if full: + filter = SearchEventFilter( + request.GET, + queryset=qs, + request=request, + ) + else: + filter = SimpleSearchEventFilter( + request.GET, + queryset=qs, + request=request, + ) + if "q" in request.GET: + categories = Category.objects.filter(name__icontains=request.GET["q"]) + s_q = remove_accents(request.GET["q"].lower()) + tags = ( + Event.objects.extra( + where=["%s ILIKE ANY (tags)"], params=[request.GET["q"]] + ) + .annotate(arr_tags=Func(F("tags"), function="unnest")) + .values_list("arr_tags", flat=True) + .distinct() + ) + tags = [ + ( + t, + emoji.demojize(remove_accents(t).lower(), delimiters=("000", "")), + ) + for t in tags + ] + tags = [t for t in tags if s_q == t[1]] + tags.sort(key=lambda x: x[1]) + tags = [t[0] for t in tags] + places = Place.objects.filter( + Q(name__icontains=request.GET["q"]) + | Q(description__icontains=request.GET["q"]) + | Q(city__icontains=request.GET["q"]) + ) + organisations = Organisation.objects.filter( + Q(name__icontains=request.GET["q"]) + | Q(description__icontains=request.GET["q"]) + ) + if request.user.is_authenticated: + rimports = RecurrentImport.objects.filter( + name__icontains=request.GET["q"] + ) + + paginator = PaginatorFilter(filter, 10, request) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/search.html", + { + "filter": filter, + "categories": categories, + "tags": tags, + "places": places, + "organisations": organisations, + "rimports": rimports, + "has_results": len(request.GET) != 0 + or (len(request.GET) > 1 and "page" in request.GET), + "paginator_filter": response, + "full": full, + }, + ) + + +def event_search_full(request): + return event_search(request, True) + + +######################### +## batch importations +######################### + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_batchimportation") +def imports(request): + rel_event = Event.objects.filter( + import_sources__contains=[OuterRef("url_source")] + ).values("pk")[:1] + paginator = Paginator( + BatchImportation.objects.all() + .order_by("-created_date") + .annotate(event_id=Subquery(rel_event)), + 30, + ) + page = request.GET.get("page") + + today = date.today() + + srcs = RecurrentImport.objects.all().values_list("source") + in_future = Event.objects.filter(Q(start_day__gte=today)) + nb_in_orphan_import = in_future.filter( + ( + Q(import_sources__isnull=False) + & (Q(modified_date__isnull=True) | Q(modified_date__lte=F("imported_date"))) + ) + & ~Q(import_sources__overlap=srcs) + ).count() + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/imports.html", + { + "paginator_filter": response, + "nb_in_orphan_import": nb_in_orphan_import, + }, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.add_batchimportation", + "agenda_culturel.run_batchimportation", + ] +) +def add_import(request): + form = BatchImportationForm() + + if request.method == "POST": + form = BatchImportationForm(request.POST) + + if form.is_valid(): + import_events_from_json.delay(form.data["json"]) + + messages.success(request, _("The import has been run successfully.")) + return HttpResponseRedirect(reverse_lazy("imports")) + + return render(request, "agenda_culturel/batchimportation_form.html", {"form": form}) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_batchimportation", + "agenda_culturel.run_batchimportation", + ] +) +def cancel_import(request, pk): + import_process = get_object_or_404(BatchImportation, pk=pk) + + if request.method == "POST": + celery_app.control.revoke(import_process.celery_id) + + import_process.status = BatchImportation.STATUS.CANCELED + import_process.save(update_fields=["status"]) + + messages.success(request, _("The import has been canceled.")) + return HttpResponseRedirect(reverse_lazy("imports")) + else: + cancel_url = reverse_lazy("imports") + return render( + request, + "agenda_culturel/cancel_import_confirm.html", + {"object": import_process, "cancel_url": cancel_url}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_batchimportation", + "agenda_culturel.run_batchimportation", + ] +) +def update_orphan_events(request): + if request.method == "POST": + # run recurrent import + update_orphan_pure_import_events.delay() + + messages.success(request, _("The orphan event update has been launched.")) + return HttpResponseRedirect(reverse_lazy("imports")) + else: + today = date.today() + + srcs = RecurrentImport.objects.all().values_list("source") + in_future = Event.objects.filter(Q(start_day__gte=today)) + nb_in_orphan_import = in_future.filter( + ( + Q(import_sources__isnull=False) + & ( + Q(modified_date__isnull=True) + | Q(modified_date__lte=F("imported_date")) + ) + ) + & ~Q(import_sources__overlap=srcs) + ).count() + return render( + request, + "agenda_culturel/run_orphan_imports_confirm.html", + {"nb_in_orphan_import": nb_in_orphan_import}, + ) + + +######################### +## recurrent importations +######################### + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_recurrentimport") +def recurrent_imports(request, status=None): + newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by( + "-created_date" + ) + + qs = ( + RecurrentImport.objects.all() + .annotate(last_run_status=Subquery(newest.values("status")[:1])) + .order_by("-pk") + ) + + nb_failed = qs.filter(last_run_status=BatchImportation.STATUS.FAILED).count() + nb_canceled = qs.filter(last_run_status=BatchImportation.STATUS.CANCELED).count() + nb_running = qs.filter(last_run_status=BatchImportation.STATUS.RUNNING).count() + nb_all = qs.count() + + if status is not None: + qs = qs.filter(last_run_status=status) + + filter = RecurrentImportFilter(request.GET, queryset=qs) + + paginator = PaginatorFilter(filter, 20, request) + + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/rimports.html", + { + "paginator_filter": response, + "filter": filter, + "nb_all": nb_all, + "nb_failed": nb_failed, + "nb_canceled": nb_canceled, + "nb_running": nb_running, + "status": status, + }, + ) + + +class RecurrentImportCreateView( + LoginRequiredMixin, PermissionRequiredMixin, CreateView +): + model = RecurrentImport + permission_required = "agenda_culturel.add_recurrentimport" + success_url = reverse_lazy("recurrent_imports") + form_class = RecurrentImportForm + + +class RecurrentImportUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = RecurrentImport + permission_required = "agenda_culturel.change_recurrentimport" + form_class = RecurrentImportForm + success_message = _("The recurrent import has been successfully modified.") + + +class RecurrentImportDeleteView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + DeleteView, +): + model = RecurrentImport + permission_required = "agenda_culturel.delete_recurrentimport" + success_url = reverse_lazy("recurrent_imports") + success_message = _("The recurrent import has been successfully deleted.") + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_recurrentimport", + "agenda_culturel.view_batchimportation", + ] +) +def view_rimport(request, pk): + obj = get_object_or_404(RecurrentImport, pk=pk) + paginator = Paginator( + BatchImportation.objects.filter(recurrentImport=pk).order_by("-created_date"), + 10, + ) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/page-rimport.html", + {"paginator_filter": response, "object": obj}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_recurrentimport", + "agenda_culturel.run_recurrentimport", + ] +) +def run_rimport(request, pk): + rimport = get_object_or_404(RecurrentImport, pk=pk) + + if request.method == "POST": + # run recurrent import + run_recurrent_import.delay(pk) + + messages.success(request, _("The import has been launched.")) + return HttpResponseRedirect(reverse_lazy("view_rimport", args=[pk])) + else: + return render( + request, + "agenda_culturel/run_rimport_confirm.html", + {"object": rimport}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_recurrentimport", + "agenda_culturel.run_recurrentimport", + ] +) +def run_all_rimports(request, status=None): + if request.method == "POST": + # run recurrent import + if status == BatchImportation.STATUS.FAILED: + run_all_recurrent_imports_failed.delay() + elif status == BatchImportation.STATUS.CANCELED: + run_all_recurrent_imports_canceled.delay() + else: + run_all_recurrent_imports.delay() + + messages.success(request, _("Imports has been launched.")) + return HttpResponseRedirect(reverse_lazy("recurrent_imports")) + else: + if status == BatchImportation.STATUS.FAILED: + return render(request, "agenda_culturel/run_failed_rimports_confirm.html") + elif status == BatchImportation.STATUS.CANCELED: + return render(request, "agenda_culturel/run_canceled_rimports_confirm.html") + else: + return render(request, "agenda_culturel/run_all_rimports_confirm.html") + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_recurrentimport", + "agenda_culturel.run_recurrentimport", + ] +) +def run_all_fb_rimports(request, status=None): + if request.method == "POST": + run_all_recurrent_imports.delay(True) + + messages.success(request, _("Facebook imports has been launched.")) + return HttpResponseRedirect(reverse_lazy("recurrent_imports")) + else: + return render(request, "agenda_culturel/run_all_fb_rimports_confirm.html") + + +######################### +## duplicated events +######################### + + +class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView): + model = DuplicatedEvents + template_name = "agenda_culturel/duplicate.html" + + +@login_required(login_url="/accounts/login/") +@permission_required( + ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] +) +def update_duplicate_event(request, pk, epk): + edup = get_object_or_404(DuplicatedEvents, pk=pk) + event = get_object_or_404(Event, pk=epk) + + form = MergeDuplicates(duplicates=edup, event=event) + + if request.method == "POST": + form = MergeDuplicates(request.POST, duplicates=edup) + if form.is_valid(): + for f in edup.get_items_comparison(): + if not f["similar"]: + selected = form.get_selected_events(f["key"]) + if selected is not None: + if isinstance(selected, list): + values = [ + x + for x in [getattr(s, f["key"]) for s in selected] + if x is not None + ] + if len(values) != 0: + if isinstance(values[0], str): + setattr(event, f["key"], "\n".join(values)) + else: + setattr(event, f["key"], sum(values, [])) + else: + if f["key"] == "organisers": + event.organisers.set(selected.organisers.all()) + else: + setattr( + event, + f["key"], + getattr(selected, f["key"]), + ) + if f["key"] == "image": + setattr( + event, + "local_image", + getattr(selected, "local_image"), + ) + + event.other_versions.fix(event) + event.save() + + messages.info(request, _("Update successfully completed.")) + return HttpResponseRedirect(event.get_absolute_url()) + + return render( + request, + "agenda_culturel/update_duplicate.html", + context={ + "form": form, + "object": edup, + "event_id": edup.get_event_index(event), + }, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] +) +def merge_duplicate(request, pk): + edup = get_object_or_404(DuplicatedEvents, pk=pk) + form = MergeDuplicates(duplicates=edup) + + if request.method == "POST": + form = MergeDuplicates(request.POST, duplicates=edup) + if form.is_valid(): + events = edup.get_duplicated() + + # build fields for the new event + new_event_data = {} + for f in edup.get_items_comparison(): + if f["similar"]: + new_event_data[f["key"]] = getattr(events[0], f["key"]) + else: + selected = form.get_selected_events(f["key"]) + if selected is None: + new_event_data[f["key"]] = None + elif isinstance(selected, list): + values = [ + x + for x in [getattr(s, f["key"]) for s in selected] + if x is not None + ] + if len(values) == 0: + new_event_data[f["key"]] = None + else: + if isinstance(values[0], str): + new_event_data[f["key"]] = "\n".join(values) + else: + new_event_data[f["key"]] = sum(values, []) + else: + new_event_data[f["key"]] = getattr(selected, f["key"]) + if f["key"] == "image" and "local_image" not in new_event_data: + new_event_data["local_image"] = getattr( + selected, "local_image" + ) + + organisers = new_event_data.pop("organisers", None) + # create a new event that merge the selected events + new_event = Event(**new_event_data) + new_event.status = Event.STATUS.PUBLISHED + new_event.other_versions = edup + new_event.save() + if organisers is not None: + new_event.organisers.set(organisers.all()) + edup.fix(new_event) + + messages.info( + request, + _("Creation of a merged event has been successfully completed."), + ) + return HttpResponseRedirect(new_event.get_absolute_url()) + + return render( + request, + "agenda_culturel/merge_duplicate.html", + context={"form": form, "object": edup}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] +) +def fix_duplicate(request, pk): + edup = get_object_or_404(DuplicatedEvents.objects.select_related(), pk=pk) + + if request.method == "POST": + form = FixDuplicates(request.POST, edup=edup) + + if form.is_valid(): + if form.is_action_no_duplicates(): + # all events are different + events = edup.get_duplicated() + + # get redirection date + if len(events) == 0: + date = None + else: + s_events = [e for e in events if not e.has_recurrences()] + if len(s_events) != 0: + s_event = s_events[0] + else: + s_event = events[0] + date = s_event.start_day + + messages.success(request, _("Events have been marked as unduplicated.")) + # delete the duplicated event (other_versions will be set to None on all events) + edup.delete() + if date is None: + return HttpResponseRedirect(reverse_lazy("home")) + else: + return HttpResponseRedirect( + reverse_lazy("day_view", args=[date.year, date.month, date.day]) + ) + + elif form.is_action_select(): + # one element has been selected to be the representative + selected = form.get_selected_event(edup) + if selected is None: + messages.error( + request, + _( + "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." + ), + ) + else: + edup.fix(selected) + messages.success( + request, + _("The selected event has been set as representative"), + ) + return HttpResponseRedirect(edup.get_absolute_url()) + elif form.is_action_remove(): + # one element is removed from the set + event = form.get_selected_event(edup) + if event is None: + messages.error( + request, + _( + "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." + ), + ) + return HttpResponseRedirect(edup.get_absolute_url()) + else: + event.other_versions = None + if edup.representative == event: + edup.representative = None + event.set_no_modification_date_changed() + event.save() + edup.save() + edup.events = [e for e in edup.events if e.pk != event.pk] + messages.success( + request, + _( + "The event has been withdrawn from the group and made independent." + ), + ) + if edup.nb_duplicated() == 1: + return HttpResponseRedirect(edup.get_absolute_url()) + else: + form = FixDuplicates(edup=edup) + elif form.is_action_update(): + # otherwise, an event will be updated using other elements + event = form.get_selected_event(edup) + if event is None: + messages.error( + request, + _( + "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." + ), + ) + return HttpResponseRedirect(edup.get_absolute_url()) + else: + return HttpResponseRedirect( + reverse_lazy("update_event", args=[edup.pk, event.pk]) + ) + else: + # otherwise, a new event will be created using a merging process + return HttpResponseRedirect( + reverse_lazy("merge_duplicate", args=[edup.pk]) + ) + else: + form = FixDuplicates(edup=edup) + + return render( + request, + "agenda_culturel/fix_duplicate.html", + context={"form": form, "object": edup}, + ) + + +class DuplicatedEventsUpdateView(LoginRequiredMixin, UpdateView): + model = DuplicatedEvents + fields = () + template_name = "agenda_culturel/fix_duplicate.html" + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_duplicatedevents") +def duplicates(request): + nb_removed = DuplicatedEvents.remove_singletons() + if nb_removed > 0: + messages.success( + request, + _("Cleaning up duplicates: {} item(s) fixed.").format(nb_removed), + ) + + filter = DuplicatedEventsFilter( + request.GET, queryset=DuplicatedEvents.objects.all().order_by("-pk") + ) + paginator = PaginatorFilter(filter, 10, request) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/duplicates.html", + { + "filter": filter, + "paginator_filter": response, + "paginator": paginator, + }, + ) + + +def set_duplicate(request, year, month, day, pk): + event = get_object_or_404(Event, pk=pk) + cday = CalendarDay(date(year, month, day)) + others = [ + e + for e in cday.get_events() + if e != event + and (event.other_versions is None or event.other_versions != e.other_versions) + and e.status != Event.STATUS.TRASH + ] + + form = SelectEventInList(events=others) + + if request.method == "POST": + form = SelectEventInList(request.POST, events=others) + if form.is_valid(): + selected = [o for o in others if o.pk == int(form.cleaned_data["event"])] + event.set_other_versions(selected) + # save them without updating modified date + event.set_no_modification_date_changed() + event.save() + if request.user.is_authenticated: + messages.success(request, _("The event was successfully duplicated.")) + return HttpResponseRedirect( + reverse_lazy("view_duplicate", args=[event.other_versions.pk]) + ) + else: + messages.info( + request, + _( + "The event has been successfully flagged as a duplicate. The moderation team will deal with your suggestion shortly." + ), + ) + return HttpResponseRedirect(event.get_absolute_url()) + + return render( + request, + "agenda_culturel/set_duplicate.html", + context={"form": form, "event": event}, + ) + + +######################### +## categorisation rules +######################### + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_categorisationrule") +def categorisation_rules(request): + paginator = Paginator( + CategorisationRule.objects.all() + .order_by("pk") + .select_related("category") + .select_related("place"), + 100, + ) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/categorisation_rules.html", + {"paginator_filter": response}, + ) + + +class CategorisationRuleCreateView( + LoginRequiredMixin, PermissionRequiredMixin, CreateView +): + model = CategorisationRule + permission_required = "agenda_culturel.add_categorisationrule" + success_url = reverse_lazy("categorisation_rules") + form_class = CategorisationRuleImportForm + + +class CategorisationRuleUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = CategorisationRule + permission_required = "agenda_culturel.change_categorisationrule" + form_class = CategorisationRuleImportForm + success_url = reverse_lazy("categorisation_rules") + success_message = _("The categorisation rule has been successfully modified.") + + +class CategorisationRuleDeleteView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + DeleteView, +): + model = CategorisationRule + permission_required = "agenda_culturel.delete_categorisationrule" + success_url = reverse_lazy("categorisation_rules") + success_message = _("The categorisation rule has been successfully deleted.") + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.apply_categorisationrules") +def apply_categorisation_rules(request): + if request.method == "POST": + form = CategorisationForm(request.POST) + if form.is_valid(): + nb = 0 + for epk, c in form.get_validated(): + e = Event.objects.get(pk=epk) + cat = Category.objects.filter(name=c).first() + e.category = cat + e.save() + nb += 1 + + if nb != 0: + if nb == 1: + messages.success( + request, + _( + "The rules were successfully applied and 1 event was categorised." + ), + ) + else: + messages.success( + request, + _( + "The rules were successfully applied and {} events were categorised." + ).format(nb), + ) + else: + messages.info( + request, + _( + "The rules were successfully applied and no events were categorised." + ), + ) + + return HttpResponseRedirect(reverse_lazy("categorisation_rules")) + else: + return render( + request, + "agenda_culturel/categorise_events_form.html", + context={"form": form}, + ) + else: + # first we check if events are not correctly categorised + to_categorise = [] + events = ( + Event.objects.filter(start_day__gte=datetime.now()) + .exclude(category=Category.get_default_category_id()) + .exclude(category=None) + .select_related("exact_location") + .select_related("category") + ) + for e in events: + c = CategorisationRule.get_category_from_rules(e) + if c and c != e.category: + to_categorise.append((e, c)) + + # then we apply rules on events without category + nb = 0 + to_save = [] + events = ( + Event.objects.filter(start_day__gte=datetime.now()) + .filter(Q(category=Category.get_default_category_id()) | Q(category=None)) + .select_related("exact_location") + .select_related("category") + ) + for e in events: + success = CategorisationRule.apply_rules(e) + if success: + nb += 1 + to_save.append(e) + + if nb != 0: + Event.objects.bulk_update(to_save, fields=["category"]) + + # set messages + if nb != 0: + if nb == 1: + messages.success( + request, + _( + "The rules were successfully applied and 1 event with default category was categorised." + ), + ) + else: + messages.success( + request, + _( + "The rules were successfully applied and {} events with default category were categorised." + ).format(nb), + ) + else: + messages.info( + request, + _( + "The rules were successfully applied and no events were categorised." + ), + ) + + if len(to_categorise) != 0: + form = CategorisationForm(events=to_categorise) + return render( + request, + "agenda_culturel/categorise_events_form.html", + context={ + "form": form, + "events": dict((e.pk, e) for e, c in to_categorise), + "categories": dict((e.pk, c) for e, c in to_categorise), + }, + ) + else: + return HttpResponseRedirect(reverse_lazy("categorisation_rules")) + + +######################### +## Places +######################### + + +class PlaceListView(ListView): + model = Place + ordering = ["name__unaccent"] + + +class PlaceListAdminView(PermissionRequiredMixin, ListView): + model = Place + paginate_by = 10 + permission_required = "agenda_culturel.add_place" + ordering = ["name__unaccent"] + template_name = "agenda_culturel/place_list_admin.html" + + +class PlaceDetailView(ListView): + model = Place + template_name = "agenda_culturel/place_detail.html" + paginate_by = 10 + + def get_queryset(self): + self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) + return ( + get_event_qs(self.request) + .filter(exact_location=self.place) + .filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + .filter(start_day__gte=datetime.now()) + .order_by("start_day") + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["object"] = self.place + return context + + +class PlaceDetailViewPast(PlaceDetailView): + def get_queryset(self): + self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) + self.past = True + return ( + get_event_qs(self.request) + .filter(exact_location=self.place) + .filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + .filter(start_day__lte=datetime.now()) + .order_by("-start_day") + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["past"] = self.past + return context + + +class UpdatePlaces: + def form_valid(self, form): + result = super().form_valid(form) + p = form.instance + + if not hasattr(self, "nb_applied"): + self.nb_applied = 0 + + # if required, find all matching events + if form.apply(): + self.nb_applied += p.associate_matching_events() + + if self.nb_applied > 1: + messages.success( + self.request, + _("{} events have been updated.").format(self.nb_applied), + ) + elif self.nb_applied == 1: + messages.success(self.request, _("1 event has been updated.")) + else: + messages.info(self.request, _("No events have been modified.")) + return result + + +class PlaceUpdateView( + UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): + model = Place + permission_required = "agenda_culturel.change_place" + success_message = _("The place has been successfully updated.") + form_class = PlaceForm + + +class PlaceCreateView( + UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): + model = Place + permission_required = "agenda_culturel.add_place" + success_message = _("The place has been successfully created.") + form_class = PlaceForm + + +class PlaceDeleteView(PermissionRequiredMixin, DeleteView): + model = Place + permission_required = "agenda_culturel.delete_place" + success_url = reverse_lazy("view_places_admin") + + +class UnknownPlacesListView(PermissionRequiredMixin, ListView): + model = Event + permission_required = "agenda_culturel.add_place" + paginate_by = 10 + ordering = ["-pk"] + template_name = "agenda_culturel/place_unknown_list.html" + queryset = Event.get_qs_events_with_unkwnon_place() + + +def fix_unknown_places(request): + # get all places + places = Place.objects.all() + # get all events without exact location + u_events = Event.get_qs_events_with_unkwnon_place() + + to_be_updated = [] + # try to find matches + for ue in u_events: + for p in places: + if p.match(ue): + ue.exact_location = p + to_be_updated.append(ue) + continue + # update events with a location + Event.objects.bulk_update(to_be_updated, fields=["exact_location"]) + + # create a success message + nb = len(to_be_updated) + if nb > 1: + messages.success(request, _("{} events have been updated.").format(nb)) + elif nb == 1: + messages.success(request, _("1 event has been updated.")) + else: + messages.info(request, _("No events have been modified.")) + + # come back to the list of places + return HttpResponseRedirect(reverse_lazy("view_unknown_places")) + + +class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): + model = Event + permission_required = ( + "agenda_culturel.change_place", + "agenda_culturel.change_event", + ) + form_class = EventAddPlaceForm + template_name = "agenda_culturel/place_unknown_form.html" + + def form_valid(self, form): + self.modified_event = form.cleaned_data.get("place") + self.add_alias = form.cleaned_data.get("add_alias") + result = super().form_valid(form) + + if form.cleaned_data.get("place"): + messages.success( + self.request, + _("The selected place has been assigned to the event."), + ) + if form.cleaned_data.get("add_alias"): + messages.success( + self.request, + _("A new alias has been added to the selected place."), + ) + + nb_applied = form.cleaned_data.get("place").associate_matching_events() + + if nb_applied > 1: + messages.success( + self.request, + _("{} events have been updated.").format(nb_applied), + ) + elif nb_applied == 1: + messages.success(self.request, _("1 event has been updated.")) + else: + messages.info(self.request, _("No events have been modified.")) + + return result + + def get_success_url(self): + if self.modified_event: + return reverse_lazy("view_unknown_places") + else: + param = "?add=1" if self.add_alias else "" + return reverse_lazy("add_place_from_event", args=[self.object.pk]) + param + + +class PlaceFromEventCreateView(PlaceCreateView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["event"] = self.event + return context + + def get_initial(self, *args, **kwargs): + initial = super().get_initial(**kwargs) + 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): + result = super().form_valid(form) + self.event.exact_location = form.instance + self.event.save(update_fields=["exact_location"]) + return result + + def get_success_url(self): + return self.event.get_absolute_url() + + +######################### +## Organisations +######################### + + +class OrganisationListView(ListView): + model = Organisation + paginate_by = 10 + ordering = ["name__unaccent"] + + +class OrganisationDetailView(ListView): + model = Organisation + template_name = "agenda_culturel/organisation_detail.html" + paginate_by = 10 + + def get_queryset(self): + self.organisation = ( + Organisation.objects.filter(pk=self.kwargs["pk"]) + .prefetch_related("organised_events") + .first() + ) + return ( + get_event_qs(self.request) + .filter(organisers__in=[self.kwargs["pk"]]) + .filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + .filter(start_day__gte=datetime.now()) + .order_by("start_day") + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["object"] = self.organisation + return context + + +class OrganisationDetailViewPast(OrganisationDetailView): + def get_queryset(self): + self.organisation = ( + Organisation.objects.filter(pk=self.kwargs["pk"]) + .prefetch_related("organised_events") + .first() + ) + self.past = True + return ( + get_event_qs(self.request) + .filter(organisers__in=[self.kwargs["pk"]]) + .filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + .filter(start_day__lte=datetime.now()) + .order_by("-start_day") + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["past"] = self.past + return context + + +class OrganisationUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): + model = Organisation + permission_required = "agenda_culturel.change_organisation" + success_message = _("The organisation has been successfully updated.") + fields = "__all__" + + +class OrganisationCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): + model = Organisation + permission_required = "agenda_culturel.add_organisation" + success_message = _("The organisation has been successfully created.") + fields = "__all__" + + +class OrganisationDeleteView(PermissionRequiredMixin, DeleteView): + model = Organisation + permission_required = "agenda_culturel.delete_organisation" + success_url = reverse_lazy("view_organisations") + + +######################### +## Tags +######################### + + +class TagUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): + model = Tag + permission_required = "agenda_culturel.change_tag" + form_class = TagForm + success_message = _("The tag has been successfully updated.") + + +class TagCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): + model = Tag + permission_required = "agenda_culturel.add_tag" + form_class = TagForm + success_message = _("The tag has been successfully created.") + + def get_initial(self, *args, **kwargs): + initial = super().get_initial(**kwargs) + if "name" in self.request.GET: + initial["name"] = self.request.GET.get("name") + return initial + + def form_valid(self, form): + Tag.clear_cache() + return super().form_valid(form) + + +class TagDeleteView(PermissionRequiredMixin, DeleteView): + model = Tag + permission_required = "agenda_culturel.delete_tag" + success_url = reverse_lazy("view_all_tags") + + +def view_tag_past(request, t): + return view_tag(request, t, True) + + +def view_tag(request, t, past=False): + now = date.today() + + qs = get_event_qs(request).filter(tags__contains=[t]) + + if past: + qs = qs.filter(start_day__lt=now).order_by("-start_day", "-start_time") + else: + qs = qs.filter(start_day__gte=now).order_by("start_day", "start_time") + + qs = qs.filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + + paginator = Paginator(qs, 10) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + 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, + "past": past, + } + return render(request, "agenda_culturel/tag.html", context) + + +def statistics(request, pk=None): + if pk is not None: + rimport = RecurrentImport.objects.filter(pk=pk) + source = rimport.values("source").first()["source"] + qs = Event.objects.filter(import_sources__contains=[source]) + else: + rimport = None + qs = Event.objects + + stats = {} + stats_months = {} + first = {} + last = {} + + ev_published = qs.filter( + Q(status=Event.STATUS.PUBLISHED) + & ( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + ) + + for v in ["start_day", "created_date__date"]: + after = 24 + last[v] = ( + date.today() + if v == "created_date__date" + else date.today() + timedelta(weeks=after) + ) + last[v] = last[v].replace( + day=_calendar.monthrange(last[v].year, last[v].month)[1] + ) + + r = 8 * 30 + if v == "start_day": + r += after * 7 + first[v] = (last[v] - timedelta(days=r)).replace(day=1) + + ev_days = ev_published.annotate(day=F(v)).filter( + Q(day__lte=last[v]) & Q(day__gte=first[v]) + ) + + stats[v] = ev_days.values("day").annotate(total=Count("day")).order_by("day") + + stats_months[v] = ( + ev_days.annotate(month=TruncMonth("day")) + .values("month") + .annotate(total=Count("month")) + .order_by("month") + ) + + nb_by_city = ( + ev_published.annotate(city=F("exact_location__city")) + .filter(city__isnull=False) + .values("city") + .annotate(total=Count("city")) + .order_by("-total") + ) + + limit = datetime.now() + timedelta(days=-30) + + stat_qs = qs.filter(start_day__gte=F("created_date")).annotate( + foresight=ExtractDay(F("start_day") - F("created_date")) + ) + + statsa = stat_qs.filter().aggregate( + minimum=Min("foresight"), + maximum=Max("foresight"), + mean=Avg("foresight"), + median=Median("foresight"), + stdev=StdDev("foresight"), + ) + + statsm = stat_qs.filter(created_date__gte=limit).aggregate( + minimum=Min("foresight"), + maximum=Max("foresight"), + mean=Avg("foresight"), + median=Median("foresight"), + stdev=StdDev("foresight"), + ) + + stats_foresight = [ + [ + _(x), + round(statsa[x], 2) if statsa[x] is not None else "-", + round(statsm[x], 2) if statsm[x] is not None else "-", + ] + for x in statsa + ] + + context = { + "stats_by_startday": stats["start_day"], + "stats_by_creation": stats["created_date__date"], + "stats_months_by_startday": stats_months["start_day"], + "stats_months_by_creation": stats_months["created_date__date"], + "first_by_startday": first["start_day"], + "last_by_startday": last["start_day"], + "first_by_creation": first["created_date__date"], + "last_by_creation": last["created_date__date"], + "nb_by_city": nb_by_city, + "stats_foresight": stats_foresight, + "object": rimport.first() if rimport else None, + } + + if pk is None: + return render(request, "agenda_culturel/statistics.html", context) + else: + return render(request, "agenda_culturel/rimport-statistics.html", context) + + +def tag_list(request): + tags = Event.get_all_tags() + r_tags = [t["tag"] for t in tags] + objects = Tag.objects.order_by("name").all() + d_objects = dict() + for o in objects: + d_objects[o.name] = o + + tags = [ + t | {"obj": d_objects[t["tag"]]} if t["tag"] in d_objects else t for t in tags + ] + tags += [ + {"obj": o, "tag": o.name, "count": 0} for o in objects if o.name not in r_tags + ] + + context = { + "tags": sorted( + tags, + key=lambda x: emoji.demojize( + remove_accents(x["tag"]).lower(), delimiters=("000", "") + ), + ) + } + return render(request, "agenda_culturel/tags.html", context) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.change_tag") +def rename_tag(request, t): + form = TagRenameForm(name=t) + + if request.method == "POST": + form = TagRenameForm(request.POST, name=t) + if form.is_valid(): + save = True + if form.cleaned_data["name"] == t: + messages.warning( + request, + _("You have not modified the tag name."), + ) + save = False + elif not form.is_force(): + if ( + Event.objects.filter( + tags__contains=[form.cleaned_data["name"]] + ).count() + > 0 + ): + if Tag.objects.filter(name=form.cleaned_data["name"]): + messages.warning( + request, + ( + _( + "This tag {} is already in use, and is described by different information from the current tag. You can force renaming by checking the corresponding option. The information associated with tag {} will be deleted, and all events associated with tag {} will be associated with tag {}." + ) + ).format( + form.cleaned_data["name"], + t, + t, + form.cleaned_data["name"], + ), + ) + else: + messages.warning( + request, + ( + _( + "This tag {} is already in use. You can force renaming by checking the corresponding option." + ) + ).format(form.cleaned_data["name"]), + ) + save = False + form = TagRenameForm(request.POST, name=t, force=True) + + if save: + # find all matching events and update them + events = Event.objects.filter(tags__contains=[t]) + new_name = form.cleaned_data["name"] + for e in events: + e.tags = [te for te in e.tags if te != t] + if new_name not in e.tags: + 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 new_name not 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, + (_("The tag {} has been successfully renamed to {}.")).format( + t, form.cleaned_data["name"] + ), + ) + return HttpResponseRedirect( + reverse_lazy("view_tag", kwargs={"t": form.cleaned_data["name"]}) + ) + + nb = Event.objects.filter(tags__contains=[t]).count() + + return render( + request, + "agenda_culturel/tag_rename_form.html", + context={"form": form, "tag": t, "nb": nb}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.delete_tag") +def delete_tag(request, t): + respage = reverse_lazy("view_all_tags") + + if request.method == "POST": + # remove tag from events + events = Event.objects.filter(tags__contains=[t]) + for e in events: + 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: + tag_object.delete() + + messages.success( + request, + (_("The tag {} has been successfully deleted.")).format(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, + }, + ) + + +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", + ) + + +class UserProfileUpdateView( + SuccessMessageMixin, + LoginRequiredMixin, + UpdateView, +): + model = UserProfile + success_message = _("Your user profile has been successfully modified.") + success_url = reverse_lazy("administration") + form_class = UserProfileForm + + def get_object(self): + return self.request.user.userprofile From 47feea263a9aa611f63dd11912611ba6e51d382e Mon Sep 17 00:00:00 2001 From: SebF Date: Mon, 24 Mar 2025 10:26:59 +0100 Subject: [PATCH 03/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20erreurs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 12 ++++++--- src/agenda_culturel/views/__init__.py | 1 + src/agenda_culturel/views/errors.py | 35 +++++++++++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 30 ----------------------- 4 files changed, 44 insertions(+), 34 deletions(-) create mode 100644 src/agenda_culturel/views/errors.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index e5d7fb7..4888b0a 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -17,6 +17,10 @@ from .sitemaps import ( ) from .models import Event, Place, Organisation, Category from .views import ( +# Errors + internal_server_error, + page_not_found, +# TODO pas encore trié home, week_view, month_view, @@ -81,8 +85,6 @@ from .views import ( event_search_full, recurrent_imports, delete_cm_spam, - page_not_found, - internal_server_error, PlaceCreateView, PlaceFromEventCreateView, moderate_from_date, @@ -136,6 +138,10 @@ sitemaps = { } urlpatterns = [ + # Errors + path("500/", internal_server_error, name="internal_server_error"), + path("404/", page_not_found, name="page_not_found"), + # TODO pas encore trié path("", home, name="home"), path("cat:/", home, name="home_category"), path( @@ -369,8 +375,6 @@ urlpatterns = [ update_duplicate_event, name="update_event", ), - path("404/", page_not_found, name="page_not_found"), - path("500/", internal_server_error, name="internal_server_error"), path( "organisme//past", OrganisationDetailViewPast.as_view(), diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 00e8edd..a11e52c 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -1 +1,2 @@ from .oldviews import * +from .errors import * diff --git a/src/agenda_culturel/views/errors.py b/src/agenda_culturel/views/errors.py new file mode 100644 index 0000000..d24216e --- /dev/null +++ b/src/agenda_culturel/views/errors.py @@ -0,0 +1,35 @@ +from django.contrib.auth.decorators import login_required, permission_required +from django.core.mail import mail_admins +from django.shortcuts import render +from django.utils.translation import gettext_lazy as _ + +from ..models import Event + + +def page_not_found(request, exception=None): + return render(request, "page-erreur.html", status=404, context={"error": 404}) + + +def internal_server_error(request): + try: + mail_admins( + request.site.name + _(": error 500"), + _("An internal error has occurred on site {} at address {}.").format( + request.site.name, request.build_absolute_uri() + ), + ) + except Exception: + pass + return render(request, "page-erreur.html", status=500, context={"error": 500}) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.change_event") +def error_next_event(request, pk): + obj = Event.objects.filter(pk=pk).first() + + return render( + request, + "agenda_culturel/event_next_error_message.html", + {"pk": pk, "object": obj}, + ) diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 72d5f15..81d1f88 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -13,7 +13,6 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache -from django.core.mail import mail_admins from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Count, F, Func, OuterRef, Q, Subquery from django.db.models.functions import TruncMonth @@ -180,23 +179,6 @@ def get_event_qs(request): return Event.objects.filter(status=Event.STATUS.PUBLISHED) -def page_not_found(request, exception=None): - return render(request, "page-erreur.html", status=404, context={"error": 404}) - - -def internal_server_error(request): - try: - mail_admins( - request.site.name + _(": error 500"), - _("An internal error has occurred on site {} at address {}.").format( - request.site.name, request.build_absolute_uri() - ), - ) - except Exception: - pass - return render(request, "page-erreur.html", status=500, context={"error": 500}) - - def thank_you(request): return render(request, "agenda_culturel/thank_you.html") @@ -681,18 +663,6 @@ class EventModerateView( return self.object.get_absolute_url() -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.change_event") -def error_next_event(request, pk): - obj = Event.objects.filter(pk=pk).first() - - return render( - request, - "agenda_culturel/event_next_error_message.html", - {"pk": pk, "object": obj}, - ) - - @login_required(login_url="/accounts/login/") @permission_required("agenda_culturel.change_event") def moderate_event_next(request, pk): From e127a25a0edf1c08a573bf67d8ecfb3c53d4f5e2 Mon Sep 17 00:00:00 2001 From: SebF Date: Mon, 24 Mar 2025 10:33:45 +0100 Subject: [PATCH 04/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20g=C3=A9n?= =?UTF-8?q?=C3=A9rales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 22 ++++---- src/agenda_culturel/views/__init__.py | 1 + .../views/general_pages_views.py | 52 +++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 44 ---------------- 4 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 src/agenda_culturel/views/general_pages_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 4888b0a..1eee369 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -20,6 +20,12 @@ from .views import ( # Errors internal_server_error, page_not_found, +# General pages + about, + import_requirements, + mentions_legales, + moderation_rules, + thank_you, # TODO pas encore trié home, week_view, @@ -76,8 +82,6 @@ from .views import ( DuplicatedEventsDetailView, StaticContentCreateView, StaticContentUpdateView, - about, - thank_you, MessageCreateView, merge_duplicate, EventCreateView, @@ -95,7 +99,6 @@ from .views import ( import_event_proxy, import_from_url, import_from_urls, - mentions_legales, view_messages, MessageUpdateView, statistics, @@ -105,8 +108,6 @@ from .views import ( CategorisationRuleDeleteView, CategorisationRuleUpdateView, apply_categorisation_rules, - moderation_rules, - import_requirements, UserProfileUpdateView, ) @@ -141,6 +142,12 @@ urlpatterns = [ # Errors path("500/", internal_server_error, name="internal_server_error"), path("404/", page_not_found, name="page_not_found"), + # General pages + path("a-propos", about, name="about"), + path("besoin-pour-import", import_requirements, name="import_requirements"), + path("mentions-legales", mentions_legales, name="mentions_legales"), + path("regles-de-moderation", moderation_rules, name="moderation_rules"), + path("merci", thank_you, name="thank_you"), # TODO pas encore trié path("", home, name="home"), path("cat:/", home, name="home_category"), @@ -291,11 +298,6 @@ urlpatterns = [ ), path("rechercher", event_search, name="event_search"), path("rechercher/complet/", event_search_full, name="event_search_full"), - path("mentions-legales", mentions_legales, name="mentions_legales"), - path("a-propos", about, name="about"), - path("regles-de-moderation", moderation_rules, name="moderation_rules"), - path("besoin-pour-import", import_requirements, name="import_requirements"), - path("merci", thank_you, name="thank_you"), path("contact", MessageCreateView.as_view(), name="contact"), path("messages", view_messages, name="messages"), path("statistiques", statistics, name="statistics"), diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index a11e52c..5bfa51d 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -1,2 +1,3 @@ from .oldviews import * from .errors import * +from .general_pages_views import * \ No newline at end of file diff --git a/src/agenda_culturel/views/general_pages_views.py b/src/agenda_culturel/views/general_pages_views.py new file mode 100644 index 0000000..d6ccbee --- /dev/null +++ b/src/agenda_culturel/views/general_pages_views.py @@ -0,0 +1,52 @@ +from django.db.models import Q +from django.shortcuts import render +from django.urls import reverse_lazy + +from ..models import RecurrentImport + +from django.utils.translation import gettext_lazy as _ + +def thank_you(request): + return render(request, "agenda_culturel/thank_you.html") + + +def mentions_legales(request): + context = { + "title": "Mentions légales", + "static_content": "mentions_legales", + "url_path": reverse_lazy("mentions_legales"), + } + return render(request, "agenda_culturel/page-single.html", context) + + +def about(request): + rimports = ( + RecurrentImport.objects.filter(~Q(recurrence=RecurrentImport.RECURRENCE.NEVER)) + .order_by("name__unaccent") + .all() + ) + context = { + "title": "À propos", + "static_content": "about", + "url_path": reverse_lazy("about"), + "rimports": rimports, + } + return render(request, "agenda_culturel/page-rimports-list.html", context) + + +def moderation_rules(request): + context = { + "title": _("Moderation rules"), + "static_content": "moderation_rules", + "url_path": reverse_lazy("moderation_rules"), + } + return render(request, "agenda_culturel/page-single.html", context) + + +def import_requirements(request): + context = { + "title": _("Import requirements"), + "static_content": "import_requirements", + "url_path": reverse_lazy("import_requirements"), + } + return render(request, "agenda_culturel/page-single.html", context) \ No newline at end of file diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 81d1f88..eb9ebfa 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -179,50 +179,6 @@ def get_event_qs(request): return Event.objects.filter(status=Event.STATUS.PUBLISHED) -def thank_you(request): - return render(request, "agenda_culturel/thank_you.html") - - -def mentions_legales(request): - context = { - "title": "Mentions légales", - "static_content": "mentions_legales", - "url_path": reverse_lazy("mentions_legales"), - } - return render(request, "agenda_culturel/page-single.html", context) - - -def about(request): - rimports = ( - RecurrentImport.objects.filter(~Q(recurrence=RecurrentImport.RECURRENCE.NEVER)) - .order_by("name__unaccent") - .all() - ) - context = { - "title": "À propos", - "static_content": "about", - "url_path": reverse_lazy("about"), - "rimports": rimports, - } - return render(request, "agenda_culturel/page-rimports-list.html", context) - - -def moderation_rules(request): - context = { - "title": _("Moderation rules"), - "static_content": "moderation_rules", - "url_path": reverse_lazy("moderation_rules"), - } - return render(request, "agenda_culturel/page-single.html", context) - - -def import_requirements(request): - context = { - "title": _("Import requirements"), - "static_content": "import_requirements", - "url_path": reverse_lazy("import_requirements"), - } - return render(request, "agenda_culturel/page-single.html", context) def home(request, cat=None): From 76610993ba8c1b000b885ec0abbb2436e4a41796 Mon Sep 17 00:00:00 2001 From: Jean-Marie Favreau Date: Mon, 24 Mar 2025 23:53:20 +0100 Subject: [PATCH 05/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20Tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- src/agenda_culturel/urls.py | 23 ++- src/agenda_culturel/views/__init__.py | 3 +- src/agenda_culturel/views/oldviews.py | 267 +----------------------- src/agenda_culturel/views/tag_views.py | 270 +++++++++++++++++++++++++ src/agenda_culturel/views/utils.py | 19 ++ 6 files changed, 305 insertions(+), 279 deletions(-) create mode 100644 src/agenda_culturel/views/tag_views.py create mode 100644 src/agenda_culturel/views/utils.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99fe2e0..fd66db1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.12 + python: python3 repos: # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - repo: https://github.com/psf/black-pre-commit-mirror diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 1eee369..27174b2 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -17,25 +17,31 @@ from .sitemaps import ( ) from .models import Event, Place, Organisation, Category from .views import ( -# Errors + # Errors internal_server_error, page_not_found, -# General pages + # General pages about, import_requirements, mentions_legales, moderation_rules, thank_you, -# TODO pas encore trié + # tags + view_tag, + view_tag_past, + TagUpdateView, + tag_list, + TagDeleteView, + rename_tag, + delete_tag, + TagCreateView, + # TODO pas encore trié home, week_view, month_view, day_view, upcoming_events, export_ical, - view_tag, - view_tag_past, - TagUpdateView, recent, administration, activite, @@ -64,11 +70,6 @@ from .views import ( cancel_import, run_all_fb_rimports, run_all_rimports, - tag_list, - TagDeleteView, - rename_tag, - delete_tag, - TagCreateView, EventDetailView, EventUpdateView, EventModerateView, diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 5bfa51d..3d380be 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -1,3 +1,4 @@ from .oldviews import * from .errors import * -from .general_pages_views import * \ No newline at end of file +from .general_pages_views import * +from .tag_views import * diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index eb9ebfa..1ebaa5b 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -79,8 +79,6 @@ from ..forms import ( RecurrentImportForm, SelectEventInList, SimpleContactForm, - TagForm, - TagRenameForm, URLSubmissionFormSet, URLSubmissionFormWithContact, UserProfileForm, @@ -97,11 +95,11 @@ from ..models import ( Place, RecurrentImport, StaticContent, - Tag, remove_accents, UserProfile, ) from ..utils import PlaceGuesser +from .utils import get_event_qs logger = logging.getLogger(__name__) @@ -161,26 +159,6 @@ class PaginatorFilter(Paginator): return page -# -# -# Useful for translation -to_be_translated = [ - _("Recurrent import name"), - _("Add another"), - _("Browse..."), - _("No file selected."), -] - - -def get_event_qs(request): - if request.user.is_authenticated: - return Event.objects.filter() - else: - return Event.objects.filter(status=Event.STATUS.PUBLISHED) - - - - def home(request, cat=None): return week_view(request, home=True, cat=cat) @@ -2622,84 +2600,6 @@ class OrganisationDeleteView(PermissionRequiredMixin, DeleteView): success_url = reverse_lazy("view_organisations") -######################### -## Tags -######################### - - -class TagUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): - model = Tag - permission_required = "agenda_culturel.change_tag" - form_class = TagForm - success_message = _("The tag has been successfully updated.") - - -class TagCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): - model = Tag - permission_required = "agenda_culturel.add_tag" - form_class = TagForm - success_message = _("The tag has been successfully created.") - - def get_initial(self, *args, **kwargs): - initial = super().get_initial(**kwargs) - if "name" in self.request.GET: - initial["name"] = self.request.GET.get("name") - return initial - - def form_valid(self, form): - Tag.clear_cache() - return super().form_valid(form) - - -class TagDeleteView(PermissionRequiredMixin, DeleteView): - model = Tag - permission_required = "agenda_culturel.delete_tag" - success_url = reverse_lazy("view_all_tags") - - -def view_tag_past(request, t): - return view_tag(request, t, True) - - -def view_tag(request, t, past=False): - now = date.today() - - qs = get_event_qs(request).filter(tags__contains=[t]) - - if past: - qs = qs.filter(start_day__lt=now).order_by("-start_day", "-start_time") - else: - qs = qs.filter(start_day__gte=now).order_by("start_day", "start_time") - - qs = qs.filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - - paginator = Paginator(qs, 10) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - 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, - "past": past, - } - return render(request, "agenda_culturel/tag.html", context) - - def statistics(request, pk=None): if pk is not None: rimport = RecurrentImport.objects.filter(pk=pk) @@ -2811,171 +2711,6 @@ def statistics(request, pk=None): return render(request, "agenda_culturel/rimport-statistics.html", context) -def tag_list(request): - tags = Event.get_all_tags() - r_tags = [t["tag"] for t in tags] - objects = Tag.objects.order_by("name").all() - d_objects = dict() - for o in objects: - d_objects[o.name] = o - - tags = [ - t | {"obj": d_objects[t["tag"]]} if t["tag"] in d_objects else t for t in tags - ] - tags += [ - {"obj": o, "tag": o.name, "count": 0} for o in objects if o.name not in r_tags - ] - - context = { - "tags": sorted( - tags, - key=lambda x: emoji.demojize( - remove_accents(x["tag"]).lower(), delimiters=("000", "") - ), - ) - } - return render(request, "agenda_culturel/tags.html", context) - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.change_tag") -def rename_tag(request, t): - form = TagRenameForm(name=t) - - if request.method == "POST": - form = TagRenameForm(request.POST, name=t) - if form.is_valid(): - save = True - if form.cleaned_data["name"] == t: - messages.warning( - request, - _("You have not modified the tag name."), - ) - save = False - elif not form.is_force(): - if ( - Event.objects.filter( - tags__contains=[form.cleaned_data["name"]] - ).count() - > 0 - ): - if Tag.objects.filter(name=form.cleaned_data["name"]): - messages.warning( - request, - ( - _( - "This tag {} is already in use, and is described by different information from the current tag. You can force renaming by checking the corresponding option. The information associated with tag {} will be deleted, and all events associated with tag {} will be associated with tag {}." - ) - ).format( - form.cleaned_data["name"], - t, - t, - form.cleaned_data["name"], - ), - ) - else: - messages.warning( - request, - ( - _( - "This tag {} is already in use. You can force renaming by checking the corresponding option." - ) - ).format(form.cleaned_data["name"]), - ) - save = False - form = TagRenameForm(request.POST, name=t, force=True) - - if save: - # find all matching events and update them - events = Event.objects.filter(tags__contains=[t]) - new_name = form.cleaned_data["name"] - for e in events: - e.tags = [te for te in e.tags if te != t] - if new_name not in e.tags: - 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 new_name not 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, - (_("The tag {} has been successfully renamed to {}.")).format( - t, form.cleaned_data["name"] - ), - ) - return HttpResponseRedirect( - reverse_lazy("view_tag", kwargs={"t": form.cleaned_data["name"]}) - ) - - nb = Event.objects.filter(tags__contains=[t]).count() - - return render( - request, - "agenda_culturel/tag_rename_form.html", - context={"form": form, "tag": t, "nb": nb}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.delete_tag") -def delete_tag(request, t): - respage = reverse_lazy("view_all_tags") - - if request.method == "POST": - # remove tag from events - events = Event.objects.filter(tags__contains=[t]) - for e in events: - 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: - tag_object.delete() - - messages.success( - request, - (_("The tag {} has been successfully deleted.")).format(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, - }, - ) - - def clear_cache(request): if request.method == "POST": cache.clear() diff --git a/src/agenda_culturel/views/tag_views.py b/src/agenda_culturel/views/tag_views.py new file mode 100644 index 0000000..6734b61 --- /dev/null +++ b/src/agenda_culturel/views/tag_views.py @@ -0,0 +1,270 @@ +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import ( + PermissionRequiredMixin, +) +from django.contrib.messages.views import SuccessMessageMixin +from django.views.generic.edit import ( + CreateView, + DeleteView, + UpdateView, +) +from ..forms import ( + TagForm, + TagRenameForm, +) +from ..models import ( + Tag, + RecurrentImport, + Event, + remove_accents, +) +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse_lazy +from datetime import date +from .utils import get_event_qs +from django.db.models import F, Q +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.shortcuts import render +from django.contrib import messages +from django.http import ( + HttpResponseRedirect, +) +import emoji + + +class TagUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): + model = Tag + permission_required = "agenda_culturel.change_tag" + form_class = TagForm + success_message = _("The tag has been successfully updated.") + + +class TagCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): + model = Tag + permission_required = "agenda_culturel.add_tag" + form_class = TagForm + success_message = _("The tag has been successfully created.") + + def get_initial(self, *args, **kwargs): + initial = super().get_initial(**kwargs) + if "name" in self.request.GET: + initial["name"] = self.request.GET.get("name") + return initial + + def form_valid(self, form): + Tag.clear_cache() + return super().form_valid(form) + + +class TagDeleteView(PermissionRequiredMixin, DeleteView): + model = Tag + permission_required = "agenda_culturel.delete_tag" + success_url = reverse_lazy("view_all_tags") + + +def view_tag_past(request, t): + return view_tag(request, t, True) + + +def view_tag(request, t, past=False): + now = date.today() + + qs = get_event_qs(request).filter(tags__contains=[t]) + + if past: + qs = qs.filter(start_day__lt=now).order_by("-start_day", "-start_time") + else: + qs = qs.filter(start_day__gte=now).order_by("start_day", "start_time") + + qs = qs.filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + + paginator = Paginator(qs, 10) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + 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, + "past": past, + } + return render(request, "agenda_culturel/tag.html", context) + + +def tag_list(request): + tags = Event.get_all_tags() + r_tags = [t["tag"] for t in tags] + objects = Tag.objects.order_by("name").all() + d_objects = dict() + for o in objects: + d_objects[o.name] = o + + tags = [ + t | {"obj": d_objects[t["tag"]]} if t["tag"] in d_objects else t for t in tags + ] + tags += [ + {"obj": o, "tag": o.name, "count": 0} for o in objects if o.name not in r_tags + ] + + context = { + "tags": sorted( + tags, + key=lambda x: emoji.demojize( + remove_accents(x["tag"]).lower(), delimiters=("000", "") + ), + ) + } + return render(request, "agenda_culturel/tags.html", context) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.change_tag") +def rename_tag(request, t): + form = TagRenameForm(name=t) + + if request.method == "POST": + form = TagRenameForm(request.POST, name=t) + if form.is_valid(): + save = True + if form.cleaned_data["name"] == t: + messages.warning( + request, + _("You have not modified the tag name."), + ) + save = False + elif not form.is_force(): + if ( + Event.objects.filter( + tags__contains=[form.cleaned_data["name"]] + ).count() + > 0 + ): + if Tag.objects.filter(name=form.cleaned_data["name"]): + messages.warning( + request, + ( + _( + "This tag {} is already in use, and is described by different information from the current tag. You can force renaming by checking the corresponding option. The information associated with tag {} will be deleted, and all events associated with tag {} will be associated with tag {}." + ) + ).format( + form.cleaned_data["name"], + t, + t, + form.cleaned_data["name"], + ), + ) + else: + messages.warning( + request, + ( + _( + "This tag {} is already in use. You can force renaming by checking the corresponding option." + ) + ).format(form.cleaned_data["name"]), + ) + save = False + form = TagRenameForm(request.POST, name=t, force=True) + + if save: + # find all matching events and update them + events = Event.objects.filter(tags__contains=[t]) + new_name = form.cleaned_data["name"] + for e in events: + e.tags = [te for te in e.tags if te != t] + if new_name not in e.tags: + 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 new_name not 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, + (_("The tag {} has been successfully renamed to {}.")).format( + t, form.cleaned_data["name"] + ), + ) + return HttpResponseRedirect( + reverse_lazy("view_tag", kwargs={"t": form.cleaned_data["name"]}) + ) + + nb = Event.objects.filter(tags__contains=[t]).count() + + return render( + request, + "agenda_culturel/tag_rename_form.html", + context={"form": form, "tag": t, "nb": nb}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.delete_tag") +def delete_tag(request, t): + respage = reverse_lazy("view_all_tags") + + if request.method == "POST": + # remove tag from events + events = Event.objects.filter(tags__contains=[t]) + for e in events: + 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: + tag_object.delete() + + messages.success( + request, + (_("The tag {} has been successfully deleted.")).format(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, + }, + ) diff --git a/src/agenda_culturel/views/utils.py b/src/agenda_culturel/views/utils.py new file mode 100644 index 0000000..a9f8b39 --- /dev/null +++ b/src/agenda_culturel/views/utils.py @@ -0,0 +1,19 @@ +from ..models import Event +from django.utils.translation import gettext_lazy as _ + +# +# +# Useful for translation +to_be_translated = [ + _("Recurrent import name"), + _("Add another"), + _("Browse..."), + _("No file selected."), +] + + +def get_event_qs(request): + if request.user.is_authenticated: + return Event.objects.filter() + else: + return Event.objects.filter(status=Event.STATUS.PUBLISHED) From 69ced85e1eb9673be80095e52435487f35c1ba3d Mon Sep 17 00:00:00 2001 From: SebF Date: Tue, 25 Mar 2025 17:08:41 +0100 Subject: [PATCH 06/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20mod=C3=A9r?= =?UTF-8?q?ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 72 ++++---- src/agenda_culturel/views/__init__.py | 1 + src/agenda_culturel/views/moderation_views.py | 168 +++++++++++++++++ src/agenda_culturel/views/oldviews.py | 174 ++---------------- 4 files changed, 221 insertions(+), 194 deletions(-) create mode 100644 src/agenda_culturel/views/moderation_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 27174b2..5c17241 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -26,7 +26,11 @@ from .views import ( mentions_legales, moderation_rules, thank_you, - # tags + # Moderation + EventModerateView, + moderate_event_next, + moderate_from_date, + # Tags view_tag, view_tag_past, TagUpdateView, @@ -72,8 +76,6 @@ from .views import ( run_all_rimports, EventDetailView, EventUpdateView, - EventModerateView, - moderate_event_next, RecurrentImportCreateView, RecurrentImportDeleteView, RecurrentImportUpdateView, @@ -92,7 +94,6 @@ from .views import ( delete_cm_spam, PlaceCreateView, PlaceFromEventCreateView, - moderate_from_date, update_from_source, change_status_event, EventDeleteView, @@ -149,6 +150,38 @@ urlpatterns = [ path("mentions-legales", mentions_legales, name="mentions_legales"), path("regles-de-moderation", moderation_rules, name="moderation_rules"), path("merci", thank_you, name="thank_you"), + # Moderation + path("moderate", EventModerateView.as_view(), name="moderate"), + path( + "event//moderate", + EventModerateView.as_view(), + name="moderate_event", + ), + path( + "event//moderate-force", + EventModerateView.as_view(), + name="moderate_event_force", + ), + path( + "event//moderate/after/", + EventModerateView.as_view(), + name="moderate_event_step", + ), + path( + "event//moderate/back/", + EventModerateView.as_view(), + name="moderate_event_backstep", + ), + path( + "event//moderate-next", + moderate_event_next, + name="moderate_event_next", + ), + path( + "moderate///", + moderate_from_date, + name="moderate_from_date", + ), # TODO pas encore trié path("", home, name="home"), path("cat:/", home, name="home_category"), @@ -218,37 +251,6 @@ urlpatterns = [ path( "event//edit-force", EventUpdateView.as_view(), name="edit_event_force" ), - path( - "event//moderate", - EventModerateView.as_view(), - name="moderate_event", - ), - path( - "event//moderate-force", - EventModerateView.as_view(), - name="moderate_event_force", - ), - path( - "event//moderate/after/", - EventModerateView.as_view(), - name="moderate_event_step", - ), - path( - "event//moderate/back/", - EventModerateView.as_view(), - name="moderate_event_backstep", - ), - path( - "event//moderate-next", - moderate_event_next, - name="moderate_event_next", - ), - path("moderate", EventModerateView.as_view(), name="moderate"), - path( - "moderate///", - moderate_from_date, - name="moderate_from_date", - ), path( "event//simple-clone/edit", EventUpdateView.as_view(), diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 3d380be..f35e9ab 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -1,4 +1,5 @@ from .oldviews import * from .errors import * from .general_pages_views import * +from .moderation_views import * from .tag_views import * diff --git a/src/agenda_culturel/views/moderation_views.py b/src/agenda_culturel/views/moderation_views.py new file mode 100644 index 0000000..c1184ca --- /dev/null +++ b/src/agenda_culturel/views/moderation_views.py @@ -0,0 +1,168 @@ +from datetime import date + +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.http import Http404, HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse_lazy +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ +from django.views.generic import UpdateView +from django.db.models import Q, F +from django.utils.timezone import datetime +from ..forms import EventModerateForm +from ..models import Event + + +class EventModerateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = Event + permission_required = "agenda_culturel.change_event" + template_name = "agenda_culturel/event_form_moderate.html" + form_class = EventModerateForm + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["is_moderation_expert"] = ( + self.request.user.userprofile.is_moderation_expert + ) + 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") and self.with_msg + else "" + ) + return mark_safe( + _('The event {} has been moderated with success.').format( + self.object.get_absolute_url(), self.object.title + ) + + txt + ) + + def is_moderate_next(self): + return "after" in self.request.path.split("/") + + def is_moderate_back(self): + return "back" in self.request.path.split("/") + + def is_moderate_force(self): + return "moderate-force" in self.request.path.split("/") + + def is_starting_moderation(self): + return "pk" not 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, opk): + # 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)) + ) + ) + else: + qs = qs.filter(Q(start_day__gte=start_day) & ~Q(pk=opk)) + + # get only possibly representative events + qs = qs.filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + + # remove trash events + qs = qs.filter(~Q(status=Event.STATUS.TRASH)) + + # sort by datetime + qs = qs.order_by("start_day", "start_time") + + return qs.first() + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.is_moderate_next(): + context["pred"] = self.kwargs["pred"] + return context + + def get_object(self, queryset=None): + if self.is_starting_moderation(): + now = datetime.now() + event = EventModerateView.get_next_event(now.date(), now.time(), None) + else: + event = super().get_object(queryset) + if event.status == Event.STATUS.DRAFT: + event.status = Event.STATUS.PUBLISHED + if self.is_moderate_back(): + pred = Event.objects.filter(pk=self.kwargs["pred"]).first() + pred.free_modification_lock(self.request.user) + if self.is_moderate_force(): + event.free_modification_lock(self.request.user, False) + return event + + def post(self, request, *args, **kwargs): + try: + return super().post(request, args, kwargs) + except Http404: + return HttpResponseRedirect( + reverse_lazy("error_next_event", args=[self.object.pk]) + ) + + 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_edit_local" in self.request.POST: + return reverse_lazy("edit_event", args=[self.object.get_local_version().pk]) + else: + return self.object.get_absolute_url() + + +@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() + # free lock + obj.free_modification_lock(request.user) + start_day = obj.start_day + start_time = obj.start_time + + next_obj = EventModerateView.get_next_event(start_day, start_time, pk) + 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, None) + return HttpResponseRedirect(reverse_lazy("moderate_event", args=[obj.pk])) diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 1ebaa5b..f0fdb6c 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -14,10 +14,13 @@ from django.contrib.auth.mixins import ( from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.db.models import Aggregate, FloatField +from django.db.models import Avg, Max, Min from django.db.models import Count, F, Func, OuterRef, Q, Subquery +from django.db.models.aggregates import StdDev +from django.db.models.functions import ExtractDay from django.db.models.functions import TruncMonth from django.http import ( - Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect, @@ -37,10 +40,6 @@ from django.views.generic.edit import ( UpdateView, ) from honeypot.decorators import check_honeypot -from django.db.models.aggregates import StdDev -from django.db.models import Avg, Max, Min -from django.db.models import Aggregate, FloatField -from django.db.models.functions import ExtractDay from ..calendar import CalendarDay, CalendarList, CalendarMonth, CalendarWeek from ..celery import app as celery_app @@ -70,7 +69,6 @@ from ..forms import ( EventAddPlaceForm, EventForm, EventFormWithContact, - EventModerateForm, FixDuplicates, MergeDuplicates, MessageEventForm, @@ -159,6 +157,17 @@ class PaginatorFilter(Paginator): return page +# +# +# Useful for translation +to_be_translated = [ + _("Recurrent import name"), + _("Add another"), + _("Browse..."), + _("No file selected."), +] + + def home(request, cat=None): return week_view(request, home=True, cat=cat) @@ -475,159 +484,6 @@ class EventUpdateView( return self.object.get_absolute_url() -class EventModerateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = Event - permission_required = "agenda_culturel.change_event" - template_name = "agenda_culturel/event_form_moderate.html" - form_class = EventModerateForm - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["is_moderation_expert"] = ( - self.request.user.userprofile.is_moderation_expert - ) - 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") and self.with_msg - else "" - ) - return mark_safe( - _('The event {} has been moderated with success.').format( - self.object.get_absolute_url(), self.object.title - ) - + txt - ) - - def is_moderate_next(self): - return "after" in self.request.path.split("/") - - def is_moderate_back(self): - return "back" in self.request.path.split("/") - - def is_moderate_force(self): - return "moderate-force" in self.request.path.split("/") - - def is_starting_moderation(self): - return "pk" not 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, opk): - # 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)) - ) - ) - else: - qs = qs.filter(Q(start_day__gte=start_day) & ~Q(pk=opk)) - - # get only possibly representative events - qs = qs.filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - - # remove trash events - qs = qs.filter(~Q(status=Event.STATUS.TRASH)) - - # sort by datetime - qs = qs.order_by("start_day", "start_time") - - return qs.first() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - if self.is_moderate_next(): - context["pred"] = self.kwargs["pred"] - return context - - def get_object(self, queryset=None): - if self.is_starting_moderation(): - now = datetime.now() - event = EventModerateView.get_next_event(now.date(), now.time(), None) - else: - event = super().get_object(queryset) - if event.status == Event.STATUS.DRAFT: - event.status = Event.STATUS.PUBLISHED - if self.is_moderate_back(): - pred = Event.objects.filter(pk=self.kwargs["pred"]).first() - pred.free_modification_lock(self.request.user) - if self.is_moderate_force(): - event.free_modification_lock(self.request.user, False) - return event - - def post(self, request, *args, **kwargs): - try: - return super().post(request, args, kwargs) - except Http404: - return HttpResponseRedirect( - reverse_lazy("error_next_event", args=[self.object.pk]) - ) - - 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_edit_local" in self.request.POST: - return reverse_lazy("edit_event", args=[self.object.get_local_version().pk]) - else: - return self.object.get_absolute_url() - - -@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() - # free lock - obj.free_modification_lock(request.user) - start_day = obj.start_day - start_time = obj.start_time - - next_obj = EventModerateView.get_next_event(start_day, start_time, pk) - 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, None) - return HttpResponseRedirect(reverse_lazy("moderate_event", args=[obj.pk])) - - class EventDeleteView( SuccessMessageMixin, PermissionRequiredMixin, From 4183b7c764d03a5693ec5ea93468ec016ef61667 Mon Sep 17 00:00:00 2001 From: SebF Date: Fri, 28 Mar 2025 15:00:33 +0100 Subject: [PATCH 07/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20organisati?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 104 +++++++++--------- src/agenda_culturel/views/__init__.py | 1 + src/agenda_culturel/views/oldviews.py | 86 --------------- .../views/organisations_views.py | 92 ++++++++++++++++ 4 files changed, 146 insertions(+), 137 deletions(-) create mode 100644 src/agenda_culturel/views/organisations_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 5c17241..1c236ce 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -30,6 +30,13 @@ from .views import ( EventModerateView, moderate_event_next, moderate_from_date, + # Organisations + OrganisationCreateView, + OrganisationDeleteView, + OrganisationDetailView, + OrganisationDetailViewPast, + OrganisationListView, + OrganisationUpdateView, # Tags view_tag, view_tag_past, @@ -49,12 +56,6 @@ from .views import ( recent, administration, activite, - OrganisationDeleteView, - OrganisationCreateView, - OrganisationDetailView, - OrganisationDetailViewPast, - OrganisationListView, - OrganisationUpdateView, PlaceDeleteView, PlaceDetailView, PlaceDetailViewPast, @@ -182,6 +183,52 @@ urlpatterns = [ moderate_from_date, name="moderate_from_date", ), + # Organisations + path( + "organisme/add", + OrganisationCreateView.as_view(), + name="add_organisation", + ), + path( + "organisme//delete", + OrganisationDeleteView.as_view(), + name="delete_organisation", + ), + path( + "organisme/-", + OrganisationDetailView.as_view(), + name="view_organisation", + ), + path( + "organisme/-", + OrganisationDetailView.as_view(), + name="view_organisation_fullname", + ), + path( + "organisme/", + OrganisationDetailView.as_view(), + name="view_organisation_shortname", + ), + path( + "organisme//past", + OrganisationDetailViewPast.as_view(), + name="view_organisation_past", + ), + path( + "organisme/-/past", + OrganisationDetailViewPast.as_view(), + name="view_organisation_past_fullname", + ), + path( + "organismes/", + OrganisationListView.as_view(), + name="view_organisations", + ), + path( + "organisme//edit", + OrganisationUpdateView.as_view(), + name="edit_organisation", + ), # TODO pas encore trié path("", home, name="home"), path("cat:/", home, name="home_category"), @@ -380,56 +427,11 @@ urlpatterns = [ update_duplicate_event, name="update_event", ), - path( - "organisme//past", - OrganisationDetailViewPast.as_view(), - name="view_organisation_past", - ), - path( - "organisme/", - OrganisationDetailView.as_view(), - name="view_organisation_shortname", - ), path( "organisme//ical", export_ical, name="export_ical_organisation", ), - path( - "organisme/-", - OrganisationDetailView.as_view(), - name="view_organisation", - ), - path( - "organisme/-/past", - OrganisationDetailViewPast.as_view(), - name="view_organisation_past_fullname", - ), - path( - "organisme/-", - OrganisationDetailView.as_view(), - name="view_organisation_fullname", - ), - path( - "organisme//edit", - OrganisationUpdateView.as_view(), - name="edit_organisation", - ), - path( - "organisme//delete", - OrganisationDeleteView.as_view(), - name="delete_organisation", - ), - path( - "organismes/", - OrganisationListView.as_view(), - name="view_organisations", - ), - path( - "organisme/add", - OrganisationCreateView.as_view(), - name="add_organisation", - ), path( "place//past", PlaceDetailViewPast.as_view(), diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index f35e9ab..b82016c 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -2,4 +2,5 @@ from .oldviews import * from .errors import * from .general_pages_views import * from .moderation_views import * +from .organisations_views import * from .tag_views import * diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index f0fdb6c..6c9fb6e 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -2370,92 +2370,6 @@ class PlaceFromEventCreateView(PlaceCreateView): return self.event.get_absolute_url() -######################### -## Organisations -######################### - - -class OrganisationListView(ListView): - model = Organisation - paginate_by = 10 - ordering = ["name__unaccent"] - - -class OrganisationDetailView(ListView): - model = Organisation - template_name = "agenda_culturel/organisation_detail.html" - paginate_by = 10 - - def get_queryset(self): - self.organisation = ( - Organisation.objects.filter(pk=self.kwargs["pk"]) - .prefetch_related("organised_events") - .first() - ) - return ( - get_event_qs(self.request) - .filter(organisers__in=[self.kwargs["pk"]]) - .filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - .filter(start_day__gte=datetime.now()) - .order_by("start_day") - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["object"] = self.organisation - return context - - -class OrganisationDetailViewPast(OrganisationDetailView): - def get_queryset(self): - self.organisation = ( - Organisation.objects.filter(pk=self.kwargs["pk"]) - .prefetch_related("organised_events") - .first() - ) - self.past = True - return ( - get_event_qs(self.request) - .filter(organisers__in=[self.kwargs["pk"]]) - .filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - .filter(start_day__lte=datetime.now()) - .order_by("-start_day") - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["past"] = self.past - return context - - -class OrganisationUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): - model = Organisation - permission_required = "agenda_culturel.change_organisation" - success_message = _("The organisation has been successfully updated.") - fields = "__all__" - - -class OrganisationCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): - model = Organisation - permission_required = "agenda_culturel.add_organisation" - success_message = _("The organisation has been successfully created.") - fields = "__all__" - - -class OrganisationDeleteView(PermissionRequiredMixin, DeleteView): - model = Organisation - permission_required = "agenda_culturel.delete_organisation" - success_url = reverse_lazy("view_organisations") - - def statistics(request, pk=None): if pk is not None: rimport = RecurrentImport.objects.filter(pk=pk) diff --git a/src/agenda_culturel/views/organisations_views.py b/src/agenda_culturel/views/organisations_views.py new file mode 100644 index 0000000..198afd9 --- /dev/null +++ b/src/agenda_culturel/views/organisations_views.py @@ -0,0 +1,92 @@ +from datetime import datetime + +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.urls import reverse_lazy +from django.db.models import F, Q +from django.views.generic import ListView, UpdateView, CreateView, DeleteView +from django.utils.translation import gettext_lazy as _ + +from . import get_event_qs +from ..models import Organisation + + +class OrganisationListView(ListView): + model = Organisation + paginate_by = 10 + ordering = ["name__unaccent"] + + +class OrganisationDetailView(ListView): + model = Organisation + template_name = "agenda_culturel/organisation_detail.html" + paginate_by = 10 + + def get_queryset(self): + self.organisation = ( + Organisation.objects.filter(pk=self.kwargs["pk"]) + .prefetch_related("organised_events") + .first() + ) + return ( + get_event_qs(self.request) + .filter(organisers__in=[self.kwargs["pk"]]) + .filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + .filter(start_day__gte=datetime.now()) + .order_by("start_day") + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["object"] = self.organisation + return context + + +class OrganisationDetailViewPast(OrganisationDetailView): + def get_queryset(self): + self.organisation = ( + Organisation.objects.filter(pk=self.kwargs["pk"]) + .prefetch_related("organised_events") + .first() + ) + self.past = True + return ( + get_event_qs(self.request) + .filter(organisers__in=[self.kwargs["pk"]]) + .filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + .filter(start_day__lte=datetime.now()) + .order_by("-start_day") + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["past"] = self.past + return context + + +class OrganisationUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): + model = Organisation + permission_required = "agenda_culturel.change_organisation" + success_message = _("The organisation has been successfully updated.") + fields = "__all__" + + +class OrganisationCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): + model = Organisation + permission_required = "agenda_culturel.add_organisation" + success_message = _("The organisation has been successfully created.") + fields = "__all__" + + +class OrganisationDeleteView(PermissionRequiredMixin, DeleteView): + model = Organisation + permission_required = "agenda_culturel.delete_organisation" + success_url = reverse_lazy("view_organisations") From 897d8c6ea966f1143288befb582d470bc30a085a Mon Sep 17 00:00:00 2001 From: SebF Date: Fri, 4 Apr 2025 10:17:59 +0200 Subject: [PATCH 08/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20places?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 106 +++++----- src/agenda_culturel/views/__init__.py | 1 + src/agenda_culturel/views/oldviews.py | 237 +-------------------- src/agenda_culturel/views/places_views.py | 243 ++++++++++++++++++++++ 4 files changed, 299 insertions(+), 288 deletions(-) create mode 100644 src/agenda_culturel/views/places_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 1c236ce..8513e5c 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -46,6 +46,18 @@ from .views import ( rename_tag, delete_tag, TagCreateView, + # Places + PlaceCreateView, + PlaceDeleteView, + PlaceDetailView, + PlaceDetailViewPast, + PlaceFromEventCreateView, + PlaceListAdminView, + PlaceListView, + PlaceUpdateView, + UnknownPlaceAddView, + UnknownPlacesListView, + fix_unknown_places, # TODO pas encore trié home, week_view, @@ -56,16 +68,7 @@ from .views import ( recent, administration, activite, - PlaceDeleteView, - PlaceDetailView, - PlaceDetailViewPast, - PlaceUpdateView, - PlaceListView, - PlaceListAdminView, - UnknownPlaceAddView, - UnknownPlacesListView, fix_duplicate, - fix_unknown_places, clear_cache, export_event_ical, MessageDeleteView, @@ -93,8 +96,6 @@ from .views import ( event_search_full, recurrent_imports, delete_cm_spam, - PlaceCreateView, - PlaceFromEventCreateView, update_from_source, change_status_event, EventDeleteView, @@ -229,6 +230,48 @@ urlpatterns = [ OrganisationUpdateView.as_view(), name="edit_organisation", ), + # Places + path("places/add", PlaceCreateView.as_view(), name="add_place"), + path("place//delete", PlaceDeleteView.as_view(), name="delete_place"), + path("place/", PlaceDetailView.as_view(), name="view_place"), + path( + "place/-", + PlaceDetailView.as_view(), + name="view_place_fullname", + ), + path( + "place//past", + PlaceDetailViewPast.as_view(), + name="view_place_past", + ), + path( + "place/-/past", + PlaceDetailViewPast.as_view(), + name="view_place_past_fullname", + ), + path( + "places/add/", + PlaceFromEventCreateView.as_view(), + name="add_place_from_event", + ), + path("places/list", PlaceListAdminView.as_view(), name="view_places_admin"), + path("places/", PlaceListView.as_view(), name="view_places"), + path("place//edit", PlaceUpdateView.as_view(), name="edit_place"), + path( + "event//addplace", + UnknownPlaceAddView.as_view(), + name="add_place_to_event", + ), + path( + "events/unknown-places", + UnknownPlacesListView.as_view(), + name="view_unknown_places", + ), + path( + "events/unknown-places/fix", + fix_unknown_places, + name="fix_unknown_places", + ), # TODO pas encore trié path("", home, name="home"), path("cat:/", home, name="home_category"), @@ -432,48 +475,7 @@ urlpatterns = [ export_ical, name="export_ical_organisation", ), - path( - "place//past", - PlaceDetailViewPast.as_view(), - name="view_place_past", - ), - path("place/", PlaceDetailView.as_view(), name="view_place"), path("place//ical", export_ical, name="export_ical_place"), - path( - "place/-/past", - PlaceDetailViewPast.as_view(), - name="view_place_past_fullname", - ), - path( - "place/-", - PlaceDetailView.as_view(), - name="view_place_fullname", - ), - path("place//edit", PlaceUpdateView.as_view(), name="edit_place"), - path("place//delete", PlaceDeleteView.as_view(), name="delete_place"), - path("places/", PlaceListView.as_view(), name="view_places"), - path("places/list", PlaceListAdminView.as_view(), name="view_places_admin"), - path("places/add", PlaceCreateView.as_view(), name="add_place"), - path( - "places/add/", - PlaceFromEventCreateView.as_view(), - name="add_place_from_event", - ), - path( - "events/unknown-places", - UnknownPlacesListView.as_view(), - name="view_unknown_places", - ), - path( - "events/unknown-places/fix", - fix_unknown_places, - name="fix_unknown_places", - ), - path( - "event//addplace", - UnknownPlaceAddView.as_view(), - name="add_place_to_event", - ), path( "event/////ical", export_event_ical, diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index b82016c..7b37988 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -3,4 +3,5 @@ from .errors import * from .general_pages_views import * from .moderation_views import * from .organisations_views import * +from .places_views import * from .tag_views import * diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 6c9fb6e..d1af9d6 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -32,7 +32,7 @@ from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.timezone import datetime from django.utils.translation import gettext_lazy as _ -from django.views.generic import DetailView, ListView +from django.views.generic import DetailView from django.views.generic.edit import ( CreateView, DeleteView, @@ -66,14 +66,12 @@ from ..forms import ( BatchImportationForm, CategorisationForm, CategorisationRuleImportForm, - EventAddPlaceForm, EventForm, EventFormWithContact, FixDuplicates, MergeDuplicates, MessageEventForm, MessageForm, - PlaceForm, RecurrentImportForm, SelectEventInList, SimpleContactForm, @@ -96,7 +94,6 @@ from ..models import ( remove_accents, UserProfile, ) -from ..utils import PlaceGuesser from .utils import get_event_qs logger = logging.getLogger(__name__) @@ -2138,238 +2135,6 @@ def apply_categorisation_rules(request): return HttpResponseRedirect(reverse_lazy("categorisation_rules")) -######################### -## Places -######################### - - -class PlaceListView(ListView): - model = Place - ordering = ["name__unaccent"] - - -class PlaceListAdminView(PermissionRequiredMixin, ListView): - model = Place - paginate_by = 10 - permission_required = "agenda_culturel.add_place" - ordering = ["name__unaccent"] - template_name = "agenda_culturel/place_list_admin.html" - - -class PlaceDetailView(ListView): - model = Place - template_name = "agenda_culturel/place_detail.html" - paginate_by = 10 - - def get_queryset(self): - self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) - return ( - get_event_qs(self.request) - .filter(exact_location=self.place) - .filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - .filter(start_day__gte=datetime.now()) - .order_by("start_day") - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["object"] = self.place - return context - - -class PlaceDetailViewPast(PlaceDetailView): - def get_queryset(self): - self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) - self.past = True - return ( - get_event_qs(self.request) - .filter(exact_location=self.place) - .filter( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - .filter(start_day__lte=datetime.now()) - .order_by("-start_day") - ) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["past"] = self.past - return context - - -class UpdatePlaces: - def form_valid(self, form): - result = super().form_valid(form) - p = form.instance - - if not hasattr(self, "nb_applied"): - self.nb_applied = 0 - - # if required, find all matching events - if form.apply(): - self.nb_applied += p.associate_matching_events() - - if self.nb_applied > 1: - messages.success( - self.request, - _("{} events have been updated.").format(self.nb_applied), - ) - elif self.nb_applied == 1: - messages.success(self.request, _("1 event has been updated.")) - else: - messages.info(self.request, _("No events have been modified.")) - return result - - -class PlaceUpdateView( - UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, UpdateView -): - model = Place - permission_required = "agenda_culturel.change_place" - success_message = _("The place has been successfully updated.") - form_class = PlaceForm - - -class PlaceCreateView( - UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, CreateView -): - model = Place - permission_required = "agenda_culturel.add_place" - success_message = _("The place has been successfully created.") - form_class = PlaceForm - - -class PlaceDeleteView(PermissionRequiredMixin, DeleteView): - model = Place - permission_required = "agenda_culturel.delete_place" - success_url = reverse_lazy("view_places_admin") - - -class UnknownPlacesListView(PermissionRequiredMixin, ListView): - model = Event - permission_required = "agenda_culturel.add_place" - paginate_by = 10 - ordering = ["-pk"] - template_name = "agenda_culturel/place_unknown_list.html" - queryset = Event.get_qs_events_with_unkwnon_place() - - -def fix_unknown_places(request): - # get all places - places = Place.objects.all() - # get all events without exact location - u_events = Event.get_qs_events_with_unkwnon_place() - - to_be_updated = [] - # try to find matches - for ue in u_events: - for p in places: - if p.match(ue): - ue.exact_location = p - to_be_updated.append(ue) - continue - # update events with a location - Event.objects.bulk_update(to_be_updated, fields=["exact_location"]) - - # create a success message - nb = len(to_be_updated) - if nb > 1: - messages.success(request, _("{} events have been updated.").format(nb)) - elif nb == 1: - messages.success(request, _("1 event has been updated.")) - else: - messages.info(request, _("No events have been modified.")) - - # come back to the list of places - return HttpResponseRedirect(reverse_lazy("view_unknown_places")) - - -class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): - model = Event - permission_required = ( - "agenda_culturel.change_place", - "agenda_culturel.change_event", - ) - form_class = EventAddPlaceForm - template_name = "agenda_culturel/place_unknown_form.html" - - def form_valid(self, form): - self.modified_event = form.cleaned_data.get("place") - self.add_alias = form.cleaned_data.get("add_alias") - result = super().form_valid(form) - - if form.cleaned_data.get("place"): - messages.success( - self.request, - _("The selected place has been assigned to the event."), - ) - if form.cleaned_data.get("add_alias"): - messages.success( - self.request, - _("A new alias has been added to the selected place."), - ) - - nb_applied = form.cleaned_data.get("place").associate_matching_events() - - if nb_applied > 1: - messages.success( - self.request, - _("{} events have been updated.").format(nb_applied), - ) - elif nb_applied == 1: - messages.success(self.request, _("1 event has been updated.")) - else: - messages.info(self.request, _("No events have been modified.")) - - return result - - def get_success_url(self): - if self.modified_event: - return reverse_lazy("view_unknown_places") - else: - param = "?add=1" if self.add_alias else "" - return reverse_lazy("add_place_from_event", args=[self.object.pk]) + param - - -class PlaceFromEventCreateView(PlaceCreateView): - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["event"] = self.event - return context - - def get_initial(self, *args, **kwargs): - initial = super().get_initial(**kwargs) - 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): - result = super().form_valid(form) - self.event.exact_location = form.instance - self.event.save(update_fields=["exact_location"]) - return result - - def get_success_url(self): - return self.event.get_absolute_url() - - def statistics(request, pk=None): if pk is not None: rimport = RecurrentImport.objects.filter(pk=pk) diff --git a/src/agenda_culturel/views/places_views.py b/src/agenda_culturel/views/places_views.py new file mode 100644 index 0000000..d838fd4 --- /dev/null +++ b/src/agenda_culturel/views/places_views.py @@ -0,0 +1,243 @@ +from datetime import datetime + +from django.contrib import messages +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.db.models import F, Q +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import ListView, UpdateView, CreateView, DeleteView + +from .utils import get_event_qs +from ..forms import PlaceForm, EventAddPlaceForm +from ..models import Place, Event +from ..utils import PlaceGuesser + + +class PlaceListView(ListView): + model = Place + ordering = ["name__unaccent"] + + +class PlaceListAdminView(PermissionRequiredMixin, ListView): + model = Place + paginate_by = 10 + permission_required = "agenda_culturel.add_place" + ordering = ["name__unaccent"] + template_name = "agenda_culturel/place_list_admin.html" + + +class PlaceDetailView(ListView): + model = Place + template_name = "agenda_culturel/place_detail.html" + paginate_by = 10 + + def get_queryset(self): + self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) + return ( + get_event_qs(self.request) + .filter(exact_location=self.place) + .filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + .filter(start_day__gte=datetime.now()) + .order_by("start_day") + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["object"] = self.place + return context + + +class PlaceDetailViewPast(PlaceDetailView): + def get_queryset(self): + self.place = get_object_or_404(Place, pk=self.kwargs["pk"]) + self.past = True + return ( + get_event_qs(self.request) + .filter(exact_location=self.place) + .filter( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + .filter(start_day__lte=datetime.now()) + .order_by("-start_day") + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["past"] = self.past + return context + + +class UpdatePlaces: + def form_valid(self, form): + result = super().form_valid(form) + p = form.instance + + if not hasattr(self, "nb_applied"): + self.nb_applied = 0 + + # if required, find all matching events + if form.apply(): + self.nb_applied += p.associate_matching_events() + + if self.nb_applied > 1: + messages.success( + self.request, + _("{} events have been updated.").format(self.nb_applied), + ) + elif self.nb_applied == 1: + messages.success(self.request, _("1 event has been updated.")) + else: + messages.info(self.request, _("No events have been modified.")) + return result + + +class PlaceUpdateView( + UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, UpdateView +): + model = Place + permission_required = "agenda_culturel.change_place" + success_message = _("The place has been successfully updated.") + form_class = PlaceForm + + +class PlaceCreateView( + UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, CreateView +): + model = Place + permission_required = "agenda_culturel.add_place" + success_message = _("The place has been successfully created.") + form_class = PlaceForm + + +class PlaceDeleteView(PermissionRequiredMixin, DeleteView): + model = Place + permission_required = "agenda_culturel.delete_place" + success_url = reverse_lazy("view_places_admin") + + +class UnknownPlacesListView(PermissionRequiredMixin, ListView): + model = Event + permission_required = "agenda_culturel.add_place" + paginate_by = 10 + ordering = ["-pk"] + template_name = "agenda_culturel/place_unknown_list.html" + queryset = Event.get_qs_events_with_unkwnon_place() + + +def fix_unknown_places(request): + # get all places + places = Place.objects.all() + # get all events without exact location + u_events = Event.get_qs_events_with_unkwnon_place() + + to_be_updated = [] + # try to find matches + for ue in u_events: + for p in places: + if p.match(ue): + ue.exact_location = p + to_be_updated.append(ue) + continue + # update events with a location + Event.objects.bulk_update(to_be_updated, fields=["exact_location"]) + + # create a success message + nb = len(to_be_updated) + if nb > 1: + messages.success(request, _("{} events have been updated.").format(nb)) + elif nb == 1: + messages.success(request, _("1 event has been updated.")) + else: + messages.info(request, _("No events have been modified.")) + + # come back to the list of places + return HttpResponseRedirect(reverse_lazy("view_unknown_places")) + + +class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): + model = Event + permission_required = ( + "agenda_culturel.change_place", + "agenda_culturel.change_event", + ) + form_class = EventAddPlaceForm + template_name = "agenda_culturel/place_unknown_form.html" + + def form_valid(self, form): + self.modified_event = form.cleaned_data.get("place") + self.add_alias = form.cleaned_data.get("add_alias") + result = super().form_valid(form) + + if form.cleaned_data.get("place"): + messages.success( + self.request, + _("The selected place has been assigned to the event."), + ) + if form.cleaned_data.get("add_alias"): + messages.success( + self.request, + _("A new alias has been added to the selected place."), + ) + + nb_applied = form.cleaned_data.get("place").associate_matching_events() + + if nb_applied > 1: + messages.success( + self.request, + _("{} events have been updated.").format(nb_applied), + ) + elif nb_applied == 1: + messages.success(self.request, _("1 event has been updated.")) + else: + messages.info(self.request, _("No events have been modified.")) + + return result + + def get_success_url(self): + if self.modified_event: + return reverse_lazy("view_unknown_places") + else: + param = "?add=1" if self.add_alias else "" + return reverse_lazy("add_place_from_event", args=[self.object.pk]) + param + + +class PlaceFromEventCreateView(PlaceCreateView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["event"] = self.event + return context + + def get_initial(self, *args, **kwargs): + initial = super().get_initial(**kwargs) + 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): + result = super().form_valid(form) + self.event.exact_location = form.instance + self.event.save(update_fields=["exact_location"]) + return result + + def get_success_url(self): + return self.event.get_absolute_url() From 5000efc3531e82273a70e3d7a8b3aa13a3d60e89 Mon Sep 17 00:00:00 2001 From: SebF Date: Fri, 4 Apr 2025 10:39:26 +0200 Subject: [PATCH 09/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20cat=C3=A9g?= =?UTF-8?q?orisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 46 +++-- src/agenda_culturel/views/__init__.py | 1 + .../views/categorisation_rules_view.py | 192 ++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 186 +---------------- 4 files changed, 218 insertions(+), 207 deletions(-) create mode 100644 src/agenda_culturel/views/categorisation_rules_view.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 8513e5c..34e6e33 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -17,6 +17,12 @@ from .sitemaps import ( ) from .models import Event, Place, Organisation, Category from .views import ( + # Categorisation rules + CategorisationRuleCreateView, + CategorisationRuleDeleteView, + CategorisationRuleUpdateView, + apply_categorisation_rules, + categorisation_rules, # Errors internal_server_error, page_not_found, @@ -84,7 +90,6 @@ from .views import ( RecurrentImportDeleteView, RecurrentImportUpdateView, run_rimport, - categorisation_rules, duplicates, DuplicatedEventsDetailView, StaticContentCreateView, @@ -108,10 +113,6 @@ from .views import ( statistics, view_rimport, update_duplicate_event, - CategorisationRuleCreateView, - CategorisationRuleDeleteView, - CategorisationRuleUpdateView, - apply_categorisation_rules, UserProfileUpdateView, ) @@ -143,6 +144,24 @@ sitemaps = { } urlpatterns = [ + # Categorisation rules + path( + "catrules/add", + CategorisationRuleCreateView.as_view(), + name="add_catrule", + ), + path( + "catrules//delete", + CategorisationRuleDeleteView.as_view(), + name="delete_catrule", + ), + path( + "catrules//edit", + CategorisationRuleUpdateView.as_view(), + name="edit_catrule", + ), + path("catrules/apply", apply_categorisation_rules, name="apply_catrules"), + path("catrules/", categorisation_rules, name="categorisation_rules"), # Errors path("500/", internal_server_error, name="internal_server_error"), path("404/", page_not_found, name="page_not_found"), @@ -440,23 +459,6 @@ urlpatterns = [ name="delete_rimport", ), path("rimports//run", run_rimport, name="run_rimport"), - path("catrules/", categorisation_rules, name="categorisation_rules"), - path( - "catrules/add", - CategorisationRuleCreateView.as_view(), - name="add_catrule", - ), - path( - "catrules//edit", - CategorisationRuleUpdateView.as_view(), - name="edit_catrule", - ), - path( - "catrules//delete", - CategorisationRuleDeleteView.as_view(), - name="delete_catrule", - ), - path("catrules/apply", apply_categorisation_rules, name="apply_catrules"), path("duplicates/", duplicates, name="duplicates"), path( "duplicates/", diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 7b37988..1281b9b 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -1,4 +1,5 @@ from .oldviews import * +from .categorisation_rules_view import * from .errors import * from .general_pages_views import * from .moderation_views import * diff --git a/src/agenda_culturel/views/categorisation_rules_view.py b/src/agenda_culturel/views/categorisation_rules_view.py new file mode 100644 index 0000000..7f83e96 --- /dev/null +++ b/src/agenda_culturel/views/categorisation_rules_view.py @@ -0,0 +1,192 @@ +from datetime import datetime + +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage +from django.db.models import Q +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import UpdateView, CreateView, DeleteView + +from ..forms import CategorisationRuleImportForm, CategorisationForm +from ..models import Event, CategorisationRule, Category + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_categorisationrule") +def categorisation_rules(request): + paginator = Paginator( + CategorisationRule.objects.all() + .order_by("pk") + .select_related("category") + .select_related("place"), + 100, + ) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/categorisation_rules.html", + {"paginator_filter": response}, + ) + + +class CategorisationRuleCreateView( + LoginRequiredMixin, PermissionRequiredMixin, CreateView +): + model = CategorisationRule + permission_required = "agenda_culturel.add_categorisationrule" + success_url = reverse_lazy("categorisation_rules") + form_class = CategorisationRuleImportForm + + +class CategorisationRuleUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = CategorisationRule + permission_required = "agenda_culturel.change_categorisationrule" + form_class = CategorisationRuleImportForm + success_url = reverse_lazy("categorisation_rules") + success_message = _("The categorisation rule has been successfully modified.") + + +class CategorisationRuleDeleteView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + DeleteView, +): + model = CategorisationRule + permission_required = "agenda_culturel.delete_categorisationrule" + success_url = reverse_lazy("categorisation_rules") + success_message = _("The categorisation rule has been successfully deleted.") + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.apply_categorisationrules") +def apply_categorisation_rules(request): + if request.method == "POST": + form = CategorisationForm(request.POST) + if form.is_valid(): + nb = 0 + for epk, c in form.get_validated(): + e = Event.objects.get(pk=epk) + cat = Category.objects.filter(name=c).first() + e.category = cat + e.save() + nb += 1 + + if nb != 0: + if nb == 1: + messages.success( + request, + _( + "The rules were successfully applied and 1 event was categorised." + ), + ) + else: + messages.success( + request, + _( + "The rules were successfully applied and {} events were categorised." + ).format(nb), + ) + else: + messages.info( + request, + _( + "The rules were successfully applied and no events were categorised." + ), + ) + + return HttpResponseRedirect(reverse_lazy("categorisation_rules")) + else: + return render( + request, + "agenda_culturel/categorise_events_form.html", + context={"form": form}, + ) + else: + # first we check if events are not correctly categorised + to_categorise = [] + events = ( + Event.objects.filter(start_day__gte=datetime.now()) + .exclude(category=Category.get_default_category_id()) + .exclude(category=None) + .select_related("exact_location") + .select_related("category") + ) + for e in events: + c = CategorisationRule.get_category_from_rules(e) + if c and c != e.category: + to_categorise.append((e, c)) + + # then we apply rules on events without category + nb = 0 + to_save = [] + events = ( + Event.objects.filter(start_day__gte=datetime.now()) + .filter(Q(category=Category.get_default_category_id()) | Q(category=None)) + .select_related("exact_location") + .select_related("category") + ) + for e in events: + success = CategorisationRule.apply_rules(e) + if success: + nb += 1 + to_save.append(e) + + if nb != 0: + Event.objects.bulk_update(to_save, fields=["category"]) + + # set messages + if nb != 0: + if nb == 1: + messages.success( + request, + _( + "The rules were successfully applied and 1 event with default category was categorised." + ), + ) + else: + messages.success( + request, + _( + "The rules were successfully applied and {} events with default category were categorised." + ).format(nb), + ) + else: + messages.info( + request, + _( + "The rules were successfully applied and no events were categorised." + ), + ) + + if len(to_categorise) != 0: + form = CategorisationForm(events=to_categorise) + return render( + request, + "agenda_culturel/categorise_events_form.html", + context={ + "form": form, + "events": dict((e.pk, e) for e, c in to_categorise), + "categories": dict((e.pk, c) for e, c in to_categorise), + }, + ) + else: + return HttpResponseRedirect(reverse_lazy("categorisation_rules")) diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index d1af9d6..1c70d5d 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -41,6 +41,7 @@ from django.views.generic.edit import ( ) from honeypot.decorators import check_honeypot +from .utils import get_event_qs from ..calendar import CalendarDay, CalendarList, CalendarMonth, CalendarWeek from ..celery import app as celery_app from ..celery import ( @@ -64,8 +65,6 @@ from ..filters import ( ) from ..forms import ( BatchImportationForm, - CategorisationForm, - CategorisationRuleImportForm, EventForm, EventFormWithContact, FixDuplicates, @@ -82,7 +81,6 @@ from ..forms import ( from ..import_tasks.extractor import Extractor from ..models import ( BatchImportation, - CategorisationRule, Category, DuplicatedEvents, Event, @@ -94,7 +92,6 @@ from ..models import ( remove_accents, UserProfile, ) -from .utils import get_event_qs logger = logging.getLogger(__name__) @@ -1954,187 +1951,6 @@ def set_duplicate(request, year, month, day, pk): ) -######################### -## categorisation rules -######################### - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_categorisationrule") -def categorisation_rules(request): - paginator = Paginator( - CategorisationRule.objects.all() - .order_by("pk") - .select_related("category") - .select_related("place"), - 100, - ) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/categorisation_rules.html", - {"paginator_filter": response}, - ) - - -class CategorisationRuleCreateView( - LoginRequiredMixin, PermissionRequiredMixin, CreateView -): - model = CategorisationRule - permission_required = "agenda_culturel.add_categorisationrule" - success_url = reverse_lazy("categorisation_rules") - form_class = CategorisationRuleImportForm - - -class CategorisationRuleUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = CategorisationRule - permission_required = "agenda_culturel.change_categorisationrule" - form_class = CategorisationRuleImportForm - success_url = reverse_lazy("categorisation_rules") - success_message = _("The categorisation rule has been successfully modified.") - - -class CategorisationRuleDeleteView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - DeleteView, -): - model = CategorisationRule - permission_required = "agenda_culturel.delete_categorisationrule" - success_url = reverse_lazy("categorisation_rules") - success_message = _("The categorisation rule has been successfully deleted.") - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.apply_categorisationrules") -def apply_categorisation_rules(request): - if request.method == "POST": - form = CategorisationForm(request.POST) - if form.is_valid(): - nb = 0 - for epk, c in form.get_validated(): - e = Event.objects.get(pk=epk) - cat = Category.objects.filter(name=c).first() - e.category = cat - e.save() - nb += 1 - - if nb != 0: - if nb == 1: - messages.success( - request, - _( - "The rules were successfully applied and 1 event was categorised." - ), - ) - else: - messages.success( - request, - _( - "The rules were successfully applied and {} events were categorised." - ).format(nb), - ) - else: - messages.info( - request, - _( - "The rules were successfully applied and no events were categorised." - ), - ) - - return HttpResponseRedirect(reverse_lazy("categorisation_rules")) - else: - return render( - request, - "agenda_culturel/categorise_events_form.html", - context={"form": form}, - ) - else: - # first we check if events are not correctly categorised - to_categorise = [] - events = ( - Event.objects.filter(start_day__gte=datetime.now()) - .exclude(category=Category.get_default_category_id()) - .exclude(category=None) - .select_related("exact_location") - .select_related("category") - ) - for e in events: - c = CategorisationRule.get_category_from_rules(e) - if c and c != e.category: - to_categorise.append((e, c)) - - # then we apply rules on events without category - nb = 0 - to_save = [] - events = ( - Event.objects.filter(start_day__gte=datetime.now()) - .filter(Q(category=Category.get_default_category_id()) | Q(category=None)) - .select_related("exact_location") - .select_related("category") - ) - for e in events: - success = CategorisationRule.apply_rules(e) - if success: - nb += 1 - to_save.append(e) - - if nb != 0: - Event.objects.bulk_update(to_save, fields=["category"]) - - # set messages - if nb != 0: - if nb == 1: - messages.success( - request, - _( - "The rules were successfully applied and 1 event with default category was categorised." - ), - ) - else: - messages.success( - request, - _( - "The rules were successfully applied and {} events with default category were categorised." - ).format(nb), - ) - else: - messages.info( - request, - _( - "The rules were successfully applied and no events were categorised." - ), - ) - - if len(to_categorise) != 0: - form = CategorisationForm(events=to_categorise) - return render( - request, - "agenda_culturel/categorise_events_form.html", - context={ - "form": form, - "events": dict((e.pk, e) for e, c in to_categorise), - "categories": dict((e.pk, c) for e, c in to_categorise), - }, - ) - else: - return HttpResponseRedirect(reverse_lazy("categorisation_rules")) - - def statistics(request, pk=None): if pk is not None: rimport = RecurrentImport.objects.filter(pk=pk) From 8aebc2d3a4d18cb4b76cbf2ffc88d0026f72e5f6 Mon Sep 17 00:00:00 2001 From: SebF Date: Fri, 4 Apr 2025 10:56:26 +0200 Subject: [PATCH 10/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20import=20b?= =?UTF-8?q?atch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 26 ++-- src/agenda_culturel/views/__init__.py | 1 + .../views/import_batch_views.py | 143 ++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 137 ----------------- 4 files changed, 158 insertions(+), 149 deletions(-) create mode 100644 src/agenda_culturel/views/import_batch_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 34e6e33..a4c3c84 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -32,6 +32,11 @@ from .views import ( mentions_legales, moderation_rules, thank_you, + # Import batch + imports, + add_import, + cancel_import, + update_orphan_events, # Moderation EventModerateView, moderate_event_next, @@ -78,10 +83,6 @@ from .views import ( clear_cache, export_event_ical, MessageDeleteView, - imports, - add_import, - update_orphan_events, - cancel_import, run_all_fb_rimports, run_all_rimports, EventDetailView, @@ -171,6 +172,15 @@ urlpatterns = [ path("mentions-legales", mentions_legales, name="mentions_legales"), path("regles-de-moderation", moderation_rules, name="moderation_rules"), path("merci", thank_you, name="thank_you"), + # Import batch + path("imports/", imports, name="imports"), + path("imports/add", add_import, name="add_import"), + path("imports//cancel", cancel_import, name="cancel_import"), + path( + "imports/orphans/run", + update_orphan_events, + name="update_orphan_events", + ), # Moderation path("moderate", EventModerateView.as_view(), name="moderate"), path( @@ -424,14 +434,6 @@ urlpatterns = [ MessageDeleteView.as_view(), name="delete_message", ), - path("imports/", imports, name="imports"), - path("imports/add", add_import, name="add_import"), - path( - "imports/orphans/run", - update_orphan_events, - name="update_orphan_events", - ), - path("imports//cancel", cancel_import, name="cancel_import"), path("rimports/", recurrent_imports, name="recurrent_imports"), path("rimports/run", run_all_rimports, name="run_all_rimports"), path("rimports/fb/run", run_all_fb_rimports, name="run_all_fb_rimports"), diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 1281b9b..2911fa1 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -2,6 +2,7 @@ from .oldviews import * from .categorisation_rules_view import * from .errors import * from .general_pages_views import * +from .import_batch_views import * from .moderation_views import * from .organisations_views import * from .places_views import * diff --git a/src/agenda_culturel/views/import_batch_views.py b/src/agenda_culturel/views/import_batch_views.py new file mode 100644 index 0000000..6b4b5c0 --- /dev/null +++ b/src/agenda_culturel/views/import_batch_views.py @@ -0,0 +1,143 @@ +from datetime import date + +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage +from django.db.models import F, Q, OuterRef, Subquery +from django.http import HttpResponseRedirect +from django.shortcuts import render, get_object_or_404 +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ + +from ..celery import app as celery_app, update_orphan_pure_import_events +from ..celery import import_events_from_json +from ..forms import BatchImportationForm +from ..models import Event, BatchImportation, RecurrentImport + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_batchimportation") +def imports(request): + rel_event = Event.objects.filter( + import_sources__contains=[OuterRef("url_source")] + ).values("pk")[:1] + paginator = Paginator( + BatchImportation.objects.all() + .order_by("-created_date") + .annotate(event_id=Subquery(rel_event)), + 30, + ) + page = request.GET.get("page") + + today = date.today() + + srcs = RecurrentImport.objects.all().values_list("source") + in_future = Event.objects.filter(Q(start_day__gte=today)) + nb_in_orphan_import = in_future.filter( + ( + Q(import_sources__isnull=False) + & (Q(modified_date__isnull=True) | Q(modified_date__lte=F("imported_date"))) + ) + & ~Q(import_sources__overlap=srcs) + ).count() + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/imports.html", + { + "paginator_filter": response, + "nb_in_orphan_import": nb_in_orphan_import, + }, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.add_batchimportation", + "agenda_culturel.run_batchimportation", + ] +) +def add_import(request): + form = BatchImportationForm() + + if request.method == "POST": + form = BatchImportationForm(request.POST) + + if form.is_valid(): + import_events_from_json.delay(form.data["json"]) + + messages.success(request, _("The import has been run successfully.")) + return HttpResponseRedirect(reverse_lazy("imports")) + + return render(request, "agenda_culturel/batchimportation_form.html", {"form": form}) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_batchimportation", + "agenda_culturel.run_batchimportation", + ] +) +def cancel_import(request, pk): + import_process = get_object_or_404(BatchImportation, pk=pk) + + if request.method == "POST": + celery_app.control.revoke(import_process.celery_id) + + import_process.status = BatchImportation.STATUS.CANCELED + import_process.save(update_fields=["status"]) + + messages.success(request, _("The import has been canceled.")) + return HttpResponseRedirect(reverse_lazy("imports")) + else: + cancel_url = reverse_lazy("imports") + return render( + request, + "agenda_culturel/cancel_import_confirm.html", + {"object": import_process, "cancel_url": cancel_url}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_batchimportation", + "agenda_culturel.run_batchimportation", + ] +) +def update_orphan_events(request): + if request.method == "POST": + # run recurrent import + update_orphan_pure_import_events.delay() + + messages.success(request, _("The orphan event update has been launched.")) + return HttpResponseRedirect(reverse_lazy("imports")) + else: + today = date.today() + + srcs = RecurrentImport.objects.all().values_list("source") + in_future = Event.objects.filter(Q(start_day__gte=today)) + nb_in_orphan_import = in_future.filter( + ( + Q(import_sources__isnull=False) + & ( + Q(modified_date__isnull=True) + | Q(modified_date__lte=F("imported_date")) + ) + ) + & ~Q(import_sources__overlap=srcs) + ).count() + return render( + request, + "agenda_culturel/run_orphan_imports_confirm.html", + {"nb_in_orphan_import": nb_in_orphan_import}, + ) diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 1c70d5d..c26225c 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -43,16 +43,13 @@ from honeypot.decorators import check_honeypot from .utils import get_event_qs from ..calendar import CalendarDay, CalendarList, CalendarMonth, CalendarWeek -from ..celery import app as celery_app from ..celery import ( - import_events_from_json, import_events_from_url, import_events_from_urls, run_all_recurrent_imports, run_all_recurrent_imports_canceled, run_all_recurrent_imports_failed, run_recurrent_import, - update_orphan_pure_import_events, ) from ..filters import ( DuplicatedEventsFilter, @@ -64,7 +61,6 @@ from ..filters import ( SimpleSearchEventFilter, ) from ..forms import ( - BatchImportationForm, EventForm, EventFormWithContact, FixDuplicates, @@ -1303,139 +1299,6 @@ def event_search_full(request): return event_search(request, True) -######################### -## batch importations -######################### - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_batchimportation") -def imports(request): - rel_event = Event.objects.filter( - import_sources__contains=[OuterRef("url_source")] - ).values("pk")[:1] - paginator = Paginator( - BatchImportation.objects.all() - .order_by("-created_date") - .annotate(event_id=Subquery(rel_event)), - 30, - ) - page = request.GET.get("page") - - today = date.today() - - srcs = RecurrentImport.objects.all().values_list("source") - in_future = Event.objects.filter(Q(start_day__gte=today)) - nb_in_orphan_import = in_future.filter( - ( - Q(import_sources__isnull=False) - & (Q(modified_date__isnull=True) | Q(modified_date__lte=F("imported_date"))) - ) - & ~Q(import_sources__overlap=srcs) - ).count() - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/imports.html", - { - "paginator_filter": response, - "nb_in_orphan_import": nb_in_orphan_import, - }, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.add_batchimportation", - "agenda_culturel.run_batchimportation", - ] -) -def add_import(request): - form = BatchImportationForm() - - if request.method == "POST": - form = BatchImportationForm(request.POST) - - if form.is_valid(): - import_events_from_json.delay(form.data["json"]) - - messages.success(request, _("The import has been run successfully.")) - return HttpResponseRedirect(reverse_lazy("imports")) - - return render(request, "agenda_culturel/batchimportation_form.html", {"form": form}) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_batchimportation", - "agenda_culturel.run_batchimportation", - ] -) -def cancel_import(request, pk): - import_process = get_object_or_404(BatchImportation, pk=pk) - - if request.method == "POST": - celery_app.control.revoke(import_process.celery_id) - - import_process.status = BatchImportation.STATUS.CANCELED - import_process.save(update_fields=["status"]) - - messages.success(request, _("The import has been canceled.")) - return HttpResponseRedirect(reverse_lazy("imports")) - else: - cancel_url = reverse_lazy("imports") - return render( - request, - "agenda_culturel/cancel_import_confirm.html", - {"object": import_process, "cancel_url": cancel_url}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_batchimportation", - "agenda_culturel.run_batchimportation", - ] -) -def update_orphan_events(request): - if request.method == "POST": - # run recurrent import - update_orphan_pure_import_events.delay() - - messages.success(request, _("The orphan event update has been launched.")) - return HttpResponseRedirect(reverse_lazy("imports")) - else: - today = date.today() - - srcs = RecurrentImport.objects.all().values_list("source") - in_future = Event.objects.filter(Q(start_day__gte=today)) - nb_in_orphan_import = in_future.filter( - ( - Q(import_sources__isnull=False) - & ( - Q(modified_date__isnull=True) - | Q(modified_date__lte=F("imported_date")) - ) - ) - & ~Q(import_sources__overlap=srcs) - ).count() - return render( - request, - "agenda_culturel/run_orphan_imports_confirm.html", - {"nb_in_orphan_import": nb_in_orphan_import}, - ) - - ######################### ## recurrent importations ######################### From b529a33b24e835d91b2b6fd21255a799a7648f9c Mon Sep 17 00:00:00 2001 From: SebF Date: Fri, 4 Apr 2025 11:08:42 +0200 Subject: [PATCH 11/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20import=20r?= =?UTF-8?q?ecurrent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 70 +++--- src/agenda_culturel/views/__init__.py | 1 + .../views/import_recurrent_views.py | 202 ++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 191 ----------------- 4 files changed, 239 insertions(+), 225 deletions(-) create mode 100644 src/agenda_culturel/views/import_recurrent_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index a4c3c84..4c89592 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -37,6 +37,15 @@ from .views import ( add_import, cancel_import, update_orphan_events, + # Import récurrent + RecurrentImportCreateView, + RecurrentImportDeleteView, + RecurrentImportUpdateView, + recurrent_imports, + run_all_fb_rimports, + run_all_rimports, + run_rimport, + view_rimport, # Moderation EventModerateView, moderate_event_next, @@ -83,14 +92,8 @@ from .views import ( clear_cache, export_event_ical, MessageDeleteView, - run_all_fb_rimports, - run_all_rimports, EventDetailView, EventUpdateView, - RecurrentImportCreateView, - RecurrentImportDeleteView, - RecurrentImportUpdateView, - run_rimport, duplicates, DuplicatedEventsDetailView, StaticContentCreateView, @@ -100,7 +103,6 @@ from .views import ( EventCreateView, event_search, event_search_full, - recurrent_imports, delete_cm_spam, update_from_source, change_status_event, @@ -112,7 +114,6 @@ from .views import ( view_messages, MessageUpdateView, statistics, - view_rimport, update_duplicate_event, UserProfileUpdateView, ) @@ -181,6 +182,33 @@ urlpatterns = [ update_orphan_events, name="update_orphan_events", ), + # Import récurrent + path("rimports/add", RecurrentImportCreateView.as_view(), name="add_rimport"), + path( + "rimports//delete", + RecurrentImportDeleteView.as_view(), + name="delete_rimport", + ), + path( + "rimports//edit", + RecurrentImportUpdateView.as_view(), + name="edit_rimport", + ), + path("rimports/", recurrent_imports, name="recurrent_imports"), + path( + "rimports/status/", + recurrent_imports, + name="recurrent_imports_status", + ), + path("rimports/fb/run", run_all_fb_rimports, name="run_all_fb_rimports"), + path("rimports/run", run_all_rimports, name="run_all_rimports"), + path( + "rimports/status//run", + run_all_rimports, + name="run_all_rimports_status", + ), + path("rimports//run", run_rimport, name="run_rimport"), + path("rimports//view", view_rimport, name="view_rimport"), # Moderation path("moderate", EventModerateView.as_view(), name="moderate"), path( @@ -434,33 +462,7 @@ urlpatterns = [ MessageDeleteView.as_view(), name="delete_message", ), - path("rimports/", recurrent_imports, name="recurrent_imports"), - path("rimports/run", run_all_rimports, name="run_all_rimports"), - path("rimports/fb/run", run_all_fb_rimports, name="run_all_fb_rimports"), - path( - "rimports/status/", - recurrent_imports, - name="recurrent_imports_status", - ), - path( - "rimports/status//run", - run_all_rimports, - name="run_all_rimports_status", - ), - path("rimports/add", RecurrentImportCreateView.as_view(), name="add_rimport"), - path("rimports//view", view_rimport, name="view_rimport"), path("rimports//stats", statistics, name="stats_rimport"), - path( - "rimports//edit", - RecurrentImportUpdateView.as_view(), - name="edit_rimport", - ), - path( - "rimports//delete", - RecurrentImportDeleteView.as_view(), - name="delete_rimport", - ), - path("rimports//run", run_rimport, name="run_rimport"), path("duplicates/", duplicates, name="duplicates"), path( "duplicates/", diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 2911fa1..507c829 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -3,6 +3,7 @@ from .categorisation_rules_view import * from .errors import * from .general_pages_views import * from .import_batch_views import * +from .import_recurrent_views import * from .moderation_views import * from .organisations_views import * from .places_views import * diff --git a/src/agenda_culturel/views/import_recurrent_views.py b/src/agenda_culturel/views/import_recurrent_views.py new file mode 100644 index 0000000..656d585 --- /dev/null +++ b/src/agenda_culturel/views/import_recurrent_views.py @@ -0,0 +1,202 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage +from django.db.models import OuterRef, Subquery +from django.http import HttpResponseRedirect +from django.shortcuts import render, get_object_or_404 +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import CreateView, UpdateView, DeleteView + +from . import PaginatorFilter +from ..celery import ( + run_all_recurrent_imports, + run_all_recurrent_imports_canceled, + run_all_recurrent_imports_failed, + run_recurrent_import, +) +from ..filters import RecurrentImportFilter +from ..forms import RecurrentImportForm +from ..models import BatchImportation, RecurrentImport + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_recurrentimport") +def recurrent_imports(request, status=None): + newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by( + "-created_date" + ) + + qs = ( + RecurrentImport.objects.all() + .annotate(last_run_status=Subquery(newest.values("status")[:1])) + .order_by("-pk") + ) + + nb_failed = qs.filter(last_run_status=BatchImportation.STATUS.FAILED).count() + nb_canceled = qs.filter(last_run_status=BatchImportation.STATUS.CANCELED).count() + nb_running = qs.filter(last_run_status=BatchImportation.STATUS.RUNNING).count() + nb_all = qs.count() + + if status is not None: + qs = qs.filter(last_run_status=status) + + filter = RecurrentImportFilter(request.GET, queryset=qs) + + paginator = PaginatorFilter(filter, 20, request) + + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/rimports.html", + { + "paginator_filter": response, + "filter": filter, + "nb_all": nb_all, + "nb_failed": nb_failed, + "nb_canceled": nb_canceled, + "nb_running": nb_running, + "status": status, + }, + ) + + +class RecurrentImportCreateView( + LoginRequiredMixin, PermissionRequiredMixin, CreateView +): + model = RecurrentImport + permission_required = "agenda_culturel.add_recurrentimport" + success_url = reverse_lazy("recurrent_imports") + form_class = RecurrentImportForm + + +class RecurrentImportUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = RecurrentImport + permission_required = "agenda_culturel.change_recurrentimport" + form_class = RecurrentImportForm + success_message = _("The recurrent import has been successfully modified.") + + +class RecurrentImportDeleteView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + DeleteView, +): + model = RecurrentImport + permission_required = "agenda_culturel.delete_recurrentimport" + success_url = reverse_lazy("recurrent_imports") + success_message = _("The recurrent import has been successfully deleted.") + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_recurrentimport", + "agenda_culturel.view_batchimportation", + ] +) +def view_rimport(request, pk): + obj = get_object_or_404(RecurrentImport, pk=pk) + paginator = Paginator( + BatchImportation.objects.filter(recurrentImport=pk).order_by("-created_date"), + 10, + ) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/page-rimport.html", + {"paginator_filter": response, "object": obj}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_recurrentimport", + "agenda_culturel.run_recurrentimport", + ] +) +def run_rimport(request, pk): + rimport = get_object_or_404(RecurrentImport, pk=pk) + + if request.method == "POST": + # run recurrent import + run_recurrent_import.delay(pk) + + messages.success(request, _("The import has been launched.")) + return HttpResponseRedirect(reverse_lazy("view_rimport", args=[pk])) + else: + return render( + request, + "agenda_culturel/run_rimport_confirm.html", + {"object": rimport}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_recurrentimport", + "agenda_culturel.run_recurrentimport", + ] +) +def run_all_rimports(request, status=None): + if request.method == "POST": + # run recurrent import + if status == BatchImportation.STATUS.FAILED: + run_all_recurrent_imports_failed.delay() + elif status == BatchImportation.STATUS.CANCELED: + run_all_recurrent_imports_canceled.delay() + else: + run_all_recurrent_imports.delay() + + messages.success(request, _("Imports has been launched.")) + return HttpResponseRedirect(reverse_lazy("recurrent_imports")) + else: + if status == BatchImportation.STATUS.FAILED: + return render(request, "agenda_culturel/run_failed_rimports_confirm.html") + elif status == BatchImportation.STATUS.CANCELED: + return render(request, "agenda_culturel/run_canceled_rimports_confirm.html") + else: + return render(request, "agenda_culturel/run_all_rimports_confirm.html") + + +@login_required(login_url="/accounts/login/") +@permission_required( + [ + "agenda_culturel.view_recurrentimport", + "agenda_culturel.run_recurrentimport", + ] +) +def run_all_fb_rimports(request, status=None): + if request.method == "POST": + run_all_recurrent_imports.delay(True) + + messages.success(request, _("Facebook imports has been launched.")) + return HttpResponseRedirect(reverse_lazy("recurrent_imports")) + else: + return render(request, "agenda_culturel/run_all_fb_rimports_confirm.html") diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index c26225c..2890b9c 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -46,17 +46,12 @@ from ..calendar import CalendarDay, CalendarList, CalendarMonth, CalendarWeek from ..celery import ( import_events_from_url, import_events_from_urls, - run_all_recurrent_imports, - run_all_recurrent_imports_canceled, - run_all_recurrent_imports_failed, - run_recurrent_import, ) from ..filters import ( DuplicatedEventsFilter, EventFilter, EventFilterAdmin, MessagesFilterAdmin, - RecurrentImportFilter, SearchEventFilter, SimpleSearchEventFilter, ) @@ -67,7 +62,6 @@ from ..forms import ( MergeDuplicates, MessageEventForm, MessageForm, - RecurrentImportForm, SelectEventInList, SimpleContactForm, URLSubmissionFormSet, @@ -1299,191 +1293,6 @@ def event_search_full(request): return event_search(request, True) -######################### -## recurrent importations -######################### - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_recurrentimport") -def recurrent_imports(request, status=None): - newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by( - "-created_date" - ) - - qs = ( - RecurrentImport.objects.all() - .annotate(last_run_status=Subquery(newest.values("status")[:1])) - .order_by("-pk") - ) - - nb_failed = qs.filter(last_run_status=BatchImportation.STATUS.FAILED).count() - nb_canceled = qs.filter(last_run_status=BatchImportation.STATUS.CANCELED).count() - nb_running = qs.filter(last_run_status=BatchImportation.STATUS.RUNNING).count() - nb_all = qs.count() - - if status is not None: - qs = qs.filter(last_run_status=status) - - filter = RecurrentImportFilter(request.GET, queryset=qs) - - paginator = PaginatorFilter(filter, 20, request) - - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/rimports.html", - { - "paginator_filter": response, - "filter": filter, - "nb_all": nb_all, - "nb_failed": nb_failed, - "nb_canceled": nb_canceled, - "nb_running": nb_running, - "status": status, - }, - ) - - -class RecurrentImportCreateView( - LoginRequiredMixin, PermissionRequiredMixin, CreateView -): - model = RecurrentImport - permission_required = "agenda_culturel.add_recurrentimport" - success_url = reverse_lazy("recurrent_imports") - form_class = RecurrentImportForm - - -class RecurrentImportUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = RecurrentImport - permission_required = "agenda_culturel.change_recurrentimport" - form_class = RecurrentImportForm - success_message = _("The recurrent import has been successfully modified.") - - -class RecurrentImportDeleteView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - DeleteView, -): - model = RecurrentImport - permission_required = "agenda_culturel.delete_recurrentimport" - success_url = reverse_lazy("recurrent_imports") - success_message = _("The recurrent import has been successfully deleted.") - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_recurrentimport", - "agenda_culturel.view_batchimportation", - ] -) -def view_rimport(request, pk): - obj = get_object_or_404(RecurrentImport, pk=pk) - paginator = Paginator( - BatchImportation.objects.filter(recurrentImport=pk).order_by("-created_date"), - 10, - ) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/page-rimport.html", - {"paginator_filter": response, "object": obj}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_recurrentimport", - "agenda_culturel.run_recurrentimport", - ] -) -def run_rimport(request, pk): - rimport = get_object_or_404(RecurrentImport, pk=pk) - - if request.method == "POST": - # run recurrent import - run_recurrent_import.delay(pk) - - messages.success(request, _("The import has been launched.")) - return HttpResponseRedirect(reverse_lazy("view_rimport", args=[pk])) - else: - return render( - request, - "agenda_culturel/run_rimport_confirm.html", - {"object": rimport}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_recurrentimport", - "agenda_culturel.run_recurrentimport", - ] -) -def run_all_rimports(request, status=None): - if request.method == "POST": - # run recurrent import - if status == BatchImportation.STATUS.FAILED: - run_all_recurrent_imports_failed.delay() - elif status == BatchImportation.STATUS.CANCELED: - run_all_recurrent_imports_canceled.delay() - else: - run_all_recurrent_imports.delay() - - messages.success(request, _("Imports has been launched.")) - return HttpResponseRedirect(reverse_lazy("recurrent_imports")) - else: - if status == BatchImportation.STATUS.FAILED: - return render(request, "agenda_culturel/run_failed_rimports_confirm.html") - elif status == BatchImportation.STATUS.CANCELED: - return render(request, "agenda_culturel/run_canceled_rimports_confirm.html") - else: - return render(request, "agenda_culturel/run_all_rimports_confirm.html") - - -@login_required(login_url="/accounts/login/") -@permission_required( - [ - "agenda_culturel.view_recurrentimport", - "agenda_culturel.run_recurrentimport", - ] -) -def run_all_fb_rimports(request, status=None): - if request.method == "POST": - run_all_recurrent_imports.delay(True) - - messages.success(request, _("Facebook imports has been launched.")) - return HttpResponseRedirect(reverse_lazy("recurrent_imports")) - else: - return render(request, "agenda_culturel/run_all_fb_rimports_confirm.html") - - ######################### ## duplicated events ######################### From 9ec22919f1b27056f9f8844912256cb62f94f375 Mon Sep 17 00:00:00 2001 From: SebF Date: Fri, 4 Apr 2025 11:26:21 +0200 Subject: [PATCH 12/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20event=20du?= =?UTF-8?q?pliqu=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 50 +-- src/agenda_culturel/views/__init__.py | 1 + .../views/event_duplicate_views.py | 344 ++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 336 +---------------- 4 files changed, 372 insertions(+), 359 deletions(-) create mode 100644 src/agenda_culturel/views/event_duplicate_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 4c89592..28fd6b8 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -26,6 +26,13 @@ from .views import ( # Errors internal_server_error, page_not_found, + # Event duplicates + DuplicatedEventsDetailView, + duplicates, + fix_duplicate, + merge_duplicate, + set_duplicate, + update_duplicate_event, # General pages about, import_requirements, @@ -88,18 +95,14 @@ from .views import ( recent, administration, activite, - fix_duplicate, clear_cache, export_event_ical, MessageDeleteView, EventDetailView, EventUpdateView, - duplicates, - DuplicatedEventsDetailView, StaticContentCreateView, StaticContentUpdateView, MessageCreateView, - merge_duplicate, EventCreateView, event_search, event_search_full, @@ -107,14 +110,12 @@ from .views import ( update_from_source, change_status_event, EventDeleteView, - set_duplicate, import_event_proxy, import_from_url, import_from_urls, view_messages, MessageUpdateView, statistics, - update_duplicate_event, UserProfileUpdateView, ) @@ -167,6 +168,25 @@ urlpatterns = [ # Errors path("500/", internal_server_error, name="internal_server_error"), path("404/", page_not_found, name="page_not_found"), + path("duplicates//merge", merge_duplicate, name="merge_duplicate"), + path( + "event/////set_duplicate", + set_duplicate, + name="set_duplicate", + ), + path( + "duplicates//update/", + update_duplicate_event, + name="update_event", + ), + # Event duplicates + path( + "duplicates/", + DuplicatedEventsDetailView.as_view(), + name="view_duplicate", + ), + path("duplicates/", duplicates, name="duplicates"), + path("duplicates//fix", fix_duplicate, name="fix_duplicate"), # General pages path("a-propos", about, name="about"), path("besoin-pour-import", import_requirements, name="import_requirements"), @@ -424,11 +444,6 @@ urlpatterns = [ name="change_status_event", ), path("event//delete", EventDeleteView.as_view(), name="delete_event"), - path( - "event/////set_duplicate", - set_duplicate, - name="set_duplicate", - ), path("ajouter", import_event_proxy, name="add_event"), path("ajouter/url", import_from_url, name="add_event_url"), path("ajouter/urls", import_from_urls, name="add_event_urls"), @@ -463,19 +478,6 @@ urlpatterns = [ name="delete_message", ), path("rimports//stats", statistics, name="stats_rimport"), - path("duplicates/", duplicates, name="duplicates"), - path( - "duplicates/", - DuplicatedEventsDetailView.as_view(), - name="view_duplicate", - ), - path("duplicates//fix", fix_duplicate, name="fix_duplicate"), - path("duplicates//merge", merge_duplicate, name="merge_duplicate"), - path( - "duplicates//update/", - update_duplicate_event, - name="update_event", - ), path( "organisme//ical", export_ical, diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 507c829..48dd34b 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -1,5 +1,6 @@ from .oldviews import * from .categorisation_rules_view import * +from .event_duplicate_views import * from .errors import * from .general_pages_views import * from .import_batch_views import * diff --git a/src/agenda_culturel/views/event_duplicate_views.py b/src/agenda_culturel/views/event_duplicate_views.py new file mode 100644 index 0000000..4bf5254 --- /dev/null +++ b/src/agenda_culturel/views/event_duplicate_views.py @@ -0,0 +1,344 @@ +from datetime import date + +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.paginator import PageNotAnInteger, EmptyPage +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404, render +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import DetailView, UpdateView + +from src.agenda_culturel.calendar import CalendarDay +from src.agenda_culturel.filters import DuplicatedEventsFilter +from src.agenda_culturel.forms import MergeDuplicates, FixDuplicates, SelectEventInList +from src.agenda_culturel.models import DuplicatedEvents, Event +from src.agenda_culturel.views import PaginatorFilter + + +class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView): + model = DuplicatedEvents + template_name = "agenda_culturel/duplicate.html" + + +@login_required(login_url="/accounts/login/") +@permission_required( + ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] +) +def update_duplicate_event(request, pk, epk): + edup = get_object_or_404(DuplicatedEvents, pk=pk) + event = get_object_or_404(Event, pk=epk) + + form = MergeDuplicates(duplicates=edup, event=event) + + if request.method == "POST": + form = MergeDuplicates(request.POST, duplicates=edup) + if form.is_valid(): + for f in edup.get_items_comparison(): + if not f["similar"]: + selected = form.get_selected_events(f["key"]) + if selected is not None: + if isinstance(selected, list): + values = [ + x + for x in [getattr(s, f["key"]) for s in selected] + if x is not None + ] + if len(values) != 0: + if isinstance(values[0], str): + setattr(event, f["key"], "\n".join(values)) + else: + setattr(event, f["key"], sum(values, [])) + else: + if f["key"] == "organisers": + event.organisers.set(selected.organisers.all()) + else: + setattr( + event, + f["key"], + getattr(selected, f["key"]), + ) + if f["key"] == "image": + setattr( + event, + "local_image", + getattr(selected, "local_image"), + ) + + event.other_versions.fix(event) + event.save() + + messages.info(request, _("Update successfully completed.")) + return HttpResponseRedirect(event.get_absolute_url()) + + return render( + request, + "agenda_culturel/update_duplicate.html", + context={ + "form": form, + "object": edup, + "event_id": edup.get_event_index(event), + }, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] +) +def merge_duplicate(request, pk): + edup = get_object_or_404(DuplicatedEvents, pk=pk) + form = MergeDuplicates(duplicates=edup) + + if request.method == "POST": + form = MergeDuplicates(request.POST, duplicates=edup) + if form.is_valid(): + events = edup.get_duplicated() + + # build fields for the new event + new_event_data = {} + for f in edup.get_items_comparison(): + if f["similar"]: + new_event_data[f["key"]] = getattr(events[0], f["key"]) + else: + selected = form.get_selected_events(f["key"]) + if selected is None: + new_event_data[f["key"]] = None + elif isinstance(selected, list): + values = [ + x + for x in [getattr(s, f["key"]) for s in selected] + if x is not None + ] + if len(values) == 0: + new_event_data[f["key"]] = None + else: + if isinstance(values[0], str): + new_event_data[f["key"]] = "\n".join(values) + else: + new_event_data[f["key"]] = sum(values, []) + else: + new_event_data[f["key"]] = getattr(selected, f["key"]) + if f["key"] == "image" and "local_image" not in new_event_data: + new_event_data["local_image"] = getattr( + selected, "local_image" + ) + + organisers = new_event_data.pop("organisers", None) + # create a new event that merge the selected events + new_event = Event(**new_event_data) + new_event.status = Event.STATUS.PUBLISHED + new_event.other_versions = edup + new_event.save() + if organisers is not None: + new_event.organisers.set(organisers.all()) + edup.fix(new_event) + + messages.info( + request, + _("Creation of a merged event has been successfully completed."), + ) + return HttpResponseRedirect(new_event.get_absolute_url()) + + return render( + request, + "agenda_culturel/merge_duplicate.html", + context={"form": form, "object": edup}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required( + ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] +) +def fix_duplicate(request, pk): + edup = get_object_or_404(DuplicatedEvents.objects.select_related(), pk=pk) + + if request.method == "POST": + form = FixDuplicates(request.POST, edup=edup) + + if form.is_valid(): + if form.is_action_no_duplicates(): + # all events are different + events = edup.get_duplicated() + + # get redirection date + if len(events) == 0: + date = None + else: + s_events = [e for e in events if not e.has_recurrences()] + if len(s_events) != 0: + s_event = s_events[0] + else: + s_event = events[0] + date = s_event.start_day + + messages.success(request, _("Events have been marked as unduplicated.")) + # delete the duplicated event (other_versions will be set to None on all events) + edup.delete() + if date is None: + return HttpResponseRedirect(reverse_lazy("home")) + else: + return HttpResponseRedirect( + reverse_lazy("day_view", args=[date.year, date.month, date.day]) + ) + + elif form.is_action_select(): + # one element has been selected to be the representative + selected = form.get_selected_event(edup) + if selected is None: + messages.error( + request, + _( + "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." + ), + ) + else: + edup.fix(selected) + messages.success( + request, + _("The selected event has been set as representative"), + ) + return HttpResponseRedirect(edup.get_absolute_url()) + elif form.is_action_remove(): + # one element is removed from the set + event = form.get_selected_event(edup) + if event is None: + messages.error( + request, + _( + "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." + ), + ) + return HttpResponseRedirect(edup.get_absolute_url()) + else: + event.other_versions = None + if edup.representative == event: + edup.representative = None + event.set_no_modification_date_changed() + event.save() + edup.save() + edup.events = [e for e in edup.events if e.pk != event.pk] + messages.success( + request, + _( + "The event has been withdrawn from the group and made independent." + ), + ) + if edup.nb_duplicated() == 1: + return HttpResponseRedirect(edup.get_absolute_url()) + else: + form = FixDuplicates(edup=edup) + elif form.is_action_update(): + # otherwise, an event will be updated using other elements + event = form.get_selected_event(edup) + if event is None: + messages.error( + request, + _( + "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." + ), + ) + return HttpResponseRedirect(edup.get_absolute_url()) + else: + return HttpResponseRedirect( + reverse_lazy("update_event", args=[edup.pk, event.pk]) + ) + else: + # otherwise, a new event will be created using a merging process + return HttpResponseRedirect( + reverse_lazy("merge_duplicate", args=[edup.pk]) + ) + else: + form = FixDuplicates(edup=edup) + + return render( + request, + "agenda_culturel/fix_duplicate.html", + context={"form": form, "object": edup}, + ) + + +class DuplicatedEventsUpdateView( + LoginRequiredMixin, UpdateView +): # Todo à supprimer, pas d’utilisation ? + model = DuplicatedEvents + fields = () + template_name = "agenda_culturel/fix_duplicate.html" + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_duplicatedevents") +def duplicates(request): + nb_removed = DuplicatedEvents.remove_singletons() + if nb_removed > 0: + messages.success( + request, + _("Cleaning up duplicates: {} item(s) fixed.").format(nb_removed), + ) + + filter = DuplicatedEventsFilter( + request.GET, queryset=DuplicatedEvents.objects.all().order_by("-pk") + ) + paginator = PaginatorFilter(filter, 10, request) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/duplicates.html", + { + "filter": filter, + "paginator_filter": response, + "paginator": paginator, + }, + ) + + +def set_duplicate(request, year, month, day, pk): + event = get_object_or_404(Event, pk=pk) + cday = CalendarDay(date(year, month, day)) + others = [ + e + for e in cday.get_events() + if e != event + and (event.other_versions is None or event.other_versions != e.other_versions) + and e.status != Event.STATUS.TRASH + ] + + form = SelectEventInList(events=others) + + if request.method == "POST": + form = SelectEventInList(request.POST, events=others) + if form.is_valid(): + selected = [o for o in others if o.pk == int(form.cleaned_data["event"])] + event.set_other_versions(selected) + # save them without updating modified date + event.set_no_modification_date_changed() + event.save() + if request.user.is_authenticated: + messages.success(request, _("The event was successfully duplicated.")) + return HttpResponseRedirect( + reverse_lazy("view_duplicate", args=[event.other_versions.pk]) + ) + else: + messages.info( + request, + _( + "The event has been successfully flagged as a duplicate. The moderation team will deal with your suggestion shortly." + ), + ) + return HttpResponseRedirect(event.get_absolute_url()) + + return render( + request, + "agenda_culturel/set_duplicate.html", + context={"form": form, "event": event}, + ) diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 2890b9c..3799dfa 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -42,13 +42,12 @@ from django.views.generic.edit import ( from honeypot.decorators import check_honeypot from .utils import get_event_qs -from ..calendar import CalendarDay, CalendarList, CalendarMonth, CalendarWeek +from ..calendar import CalendarList, CalendarMonth, CalendarWeek from ..celery import ( import_events_from_url, import_events_from_urls, ) from ..filters import ( - DuplicatedEventsFilter, EventFilter, EventFilterAdmin, MessagesFilterAdmin, @@ -58,11 +57,8 @@ from ..filters import ( from ..forms import ( EventForm, EventFormWithContact, - FixDuplicates, - MergeDuplicates, MessageEventForm, MessageForm, - SelectEventInList, SimpleContactForm, URLSubmissionFormSet, URLSubmissionFormWithContact, @@ -1293,336 +1289,6 @@ def event_search_full(request): return event_search(request, True) -######################### -## duplicated events -######################### - - -class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView): - model = DuplicatedEvents - template_name = "agenda_culturel/duplicate.html" - - -@login_required(login_url="/accounts/login/") -@permission_required( - ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] -) -def update_duplicate_event(request, pk, epk): - edup = get_object_or_404(DuplicatedEvents, pk=pk) - event = get_object_or_404(Event, pk=epk) - - form = MergeDuplicates(duplicates=edup, event=event) - - if request.method == "POST": - form = MergeDuplicates(request.POST, duplicates=edup) - if form.is_valid(): - for f in edup.get_items_comparison(): - if not f["similar"]: - selected = form.get_selected_events(f["key"]) - if selected is not None: - if isinstance(selected, list): - values = [ - x - for x in [getattr(s, f["key"]) for s in selected] - if x is not None - ] - if len(values) != 0: - if isinstance(values[0], str): - setattr(event, f["key"], "\n".join(values)) - else: - setattr(event, f["key"], sum(values, [])) - else: - if f["key"] == "organisers": - event.organisers.set(selected.organisers.all()) - else: - setattr( - event, - f["key"], - getattr(selected, f["key"]), - ) - if f["key"] == "image": - setattr( - event, - "local_image", - getattr(selected, "local_image"), - ) - - event.other_versions.fix(event) - event.save() - - messages.info(request, _("Update successfully completed.")) - return HttpResponseRedirect(event.get_absolute_url()) - - return render( - request, - "agenda_culturel/update_duplicate.html", - context={ - "form": form, - "object": edup, - "event_id": edup.get_event_index(event), - }, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] -) -def merge_duplicate(request, pk): - edup = get_object_or_404(DuplicatedEvents, pk=pk) - form = MergeDuplicates(duplicates=edup) - - if request.method == "POST": - form = MergeDuplicates(request.POST, duplicates=edup) - if form.is_valid(): - events = edup.get_duplicated() - - # build fields for the new event - new_event_data = {} - for f in edup.get_items_comparison(): - if f["similar"]: - new_event_data[f["key"]] = getattr(events[0], f["key"]) - else: - selected = form.get_selected_events(f["key"]) - if selected is None: - new_event_data[f["key"]] = None - elif isinstance(selected, list): - values = [ - x - for x in [getattr(s, f["key"]) for s in selected] - if x is not None - ] - if len(values) == 0: - new_event_data[f["key"]] = None - else: - if isinstance(values[0], str): - new_event_data[f["key"]] = "\n".join(values) - else: - new_event_data[f["key"]] = sum(values, []) - else: - new_event_data[f["key"]] = getattr(selected, f["key"]) - if f["key"] == "image" and "local_image" not in new_event_data: - new_event_data["local_image"] = getattr( - selected, "local_image" - ) - - organisers = new_event_data.pop("organisers", None) - # create a new event that merge the selected events - new_event = Event(**new_event_data) - new_event.status = Event.STATUS.PUBLISHED - new_event.other_versions = edup - new_event.save() - if organisers is not None: - new_event.organisers.set(organisers.all()) - edup.fix(new_event) - - messages.info( - request, - _("Creation of a merged event has been successfully completed."), - ) - return HttpResponseRedirect(new_event.get_absolute_url()) - - return render( - request, - "agenda_culturel/merge_duplicate.html", - context={"form": form, "object": edup}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required( - ["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"] -) -def fix_duplicate(request, pk): - edup = get_object_or_404(DuplicatedEvents.objects.select_related(), pk=pk) - - if request.method == "POST": - form = FixDuplicates(request.POST, edup=edup) - - if form.is_valid(): - if form.is_action_no_duplicates(): - # all events are different - events = edup.get_duplicated() - - # get redirection date - if len(events) == 0: - date = None - else: - s_events = [e for e in events if not e.has_recurrences()] - if len(s_events) != 0: - s_event = s_events[0] - else: - s_event = events[0] - date = s_event.start_day - - messages.success(request, _("Events have been marked as unduplicated.")) - # delete the duplicated event (other_versions will be set to None on all events) - edup.delete() - if date is None: - return HttpResponseRedirect(reverse_lazy("home")) - else: - return HttpResponseRedirect( - reverse_lazy("day_view", args=[date.year, date.month, date.day]) - ) - - elif form.is_action_select(): - # one element has been selected to be the representative - selected = form.get_selected_event(edup) - if selected is None: - messages.error( - request, - _( - "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." - ), - ) - else: - edup.fix(selected) - messages.success( - request, - _("The selected event has been set as representative"), - ) - return HttpResponseRedirect(edup.get_absolute_url()) - elif form.is_action_remove(): - # one element is removed from the set - event = form.get_selected_event(edup) - if event is None: - messages.error( - request, - _( - "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." - ), - ) - return HttpResponseRedirect(edup.get_absolute_url()) - else: - event.other_versions = None - if edup.representative == event: - edup.representative = None - event.set_no_modification_date_changed() - event.save() - edup.save() - edup.events = [e for e in edup.events if e.pk != event.pk] - messages.success( - request, - _( - "The event has been withdrawn from the group and made independent." - ), - ) - if edup.nb_duplicated() == 1: - return HttpResponseRedirect(edup.get_absolute_url()) - else: - form = FixDuplicates(edup=edup) - elif form.is_action_update(): - # otherwise, an event will be updated using other elements - event = form.get_selected_event(edup) - if event is None: - messages.error( - request, - _( - "The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime." - ), - ) - return HttpResponseRedirect(edup.get_absolute_url()) - else: - return HttpResponseRedirect( - reverse_lazy("update_event", args=[edup.pk, event.pk]) - ) - else: - # otherwise, a new event will be created using a merging process - return HttpResponseRedirect( - reverse_lazy("merge_duplicate", args=[edup.pk]) - ) - else: - form = FixDuplicates(edup=edup) - - return render( - request, - "agenda_culturel/fix_duplicate.html", - context={"form": form, "object": edup}, - ) - - -class DuplicatedEventsUpdateView(LoginRequiredMixin, UpdateView): - model = DuplicatedEvents - fields = () - template_name = "agenda_culturel/fix_duplicate.html" - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_duplicatedevents") -def duplicates(request): - nb_removed = DuplicatedEvents.remove_singletons() - if nb_removed > 0: - messages.success( - request, - _("Cleaning up duplicates: {} item(s) fixed.").format(nb_removed), - ) - - filter = DuplicatedEventsFilter( - request.GET, queryset=DuplicatedEvents.objects.all().order_by("-pk") - ) - paginator = PaginatorFilter(filter, 10, request) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/duplicates.html", - { - "filter": filter, - "paginator_filter": response, - "paginator": paginator, - }, - ) - - -def set_duplicate(request, year, month, day, pk): - event = get_object_or_404(Event, pk=pk) - cday = CalendarDay(date(year, month, day)) - others = [ - e - for e in cday.get_events() - if e != event - and (event.other_versions is None or event.other_versions != e.other_versions) - and e.status != Event.STATUS.TRASH - ] - - form = SelectEventInList(events=others) - - if request.method == "POST": - form = SelectEventInList(request.POST, events=others) - if form.is_valid(): - selected = [o for o in others if o.pk == int(form.cleaned_data["event"])] - event.set_other_versions(selected) - # save them without updating modified date - event.set_no_modification_date_changed() - event.save() - if request.user.is_authenticated: - messages.success(request, _("The event was successfully duplicated.")) - return HttpResponseRedirect( - reverse_lazy("view_duplicate", args=[event.other_versions.pk]) - ) - else: - messages.info( - request, - _( - "The event has been successfully flagged as a duplicate. The moderation team will deal with your suggestion shortly." - ), - ) - return HttpResponseRedirect(event.get_absolute_url()) - - return render( - request, - "agenda_culturel/set_duplicate.html", - context={"form": form, "event": event}, - ) - - def statistics(request, pk=None): if pk is not None: rimport = RecurrentImport.objects.filter(pk=pk) From 650678b1f3a9762b15f4db926c287557a9419f14 Mon Sep 17 00:00:00 2001 From: SebF Date: Mon, 14 Apr 2025 17:45:29 +0200 Subject: [PATCH 13/20] ajout de pages aux vues generales --- src/agenda_culturel/urls.py | 20 +- .../views/general_pages_views.py | 211 +++++++++++++++++- src/agenda_culturel/views/oldviews.py | 198 +--------------- 3 files changed, 217 insertions(+), 212 deletions(-) diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 28fd6b8..66bd2c3 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -35,15 +35,20 @@ from .views import ( update_duplicate_event, # General pages about, + activite, + delete_cm_spam, + home, import_requirements, mentions_legales, moderation_rules, + statistics, thank_you, # Import batch imports, add_import, cancel_import, update_orphan_events, + view_messages, # Import récurrent RecurrentImportCreateView, RecurrentImportDeleteView, @@ -86,7 +91,6 @@ from .views import ( UnknownPlacesListView, fix_unknown_places, # TODO pas encore trié - home, week_view, month_view, day_view, @@ -94,7 +98,6 @@ from .views import ( export_ical, recent, administration, - activite, clear_cache, export_event_ical, MessageDeleteView, @@ -106,16 +109,13 @@ from .views import ( EventCreateView, event_search, event_search_full, - delete_cm_spam, update_from_source, change_status_event, EventDeleteView, import_event_proxy, import_from_url, import_from_urls, - view_messages, MessageUpdateView, - statistics, UserProfileUpdateView, ) @@ -189,10 +189,15 @@ urlpatterns = [ path("duplicates//fix", fix_duplicate, name="fix_duplicate"), # General pages path("a-propos", about, name="about"), + path("activite/", activite, name="activite"), + path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"), + path("", home, name="home"), path("besoin-pour-import", import_requirements, name="import_requirements"), path("mentions-legales", mentions_legales, name="mentions_legales"), path("regles-de-moderation", moderation_rules, name="moderation_rules"), + path("statistiques", statistics, name="statistics"), path("merci", thank_you, name="thank_you"), + path("messages", view_messages, name="messages"), # Import batch path("imports/", imports, name="imports"), path("imports/add", add_import, name="add_import"), @@ -350,7 +355,6 @@ urlpatterns = [ name="fix_unknown_places", ), # TODO pas encore trié - path("", home, name="home"), path("cat:/", home, name="home_category"), path( "cat:/semaine///", @@ -407,7 +411,6 @@ urlpatterns = [ path("tags/add", TagCreateView.as_view(), name="add_tag"), path("recent/", recent, name="recent"), path("administration/", administration, name="administration"), - path("activite/", activite, name="activite"), path( "event////-", EventDetailView.as_view(), @@ -464,9 +467,6 @@ urlpatterns = [ path("rechercher", event_search, name="event_search"), path("rechercher/complet/", event_search_full, name="event_search_full"), path("contact", MessageCreateView.as_view(), name="contact"), - path("messages", view_messages, name="messages"), - path("statistiques", statistics, name="statistics"), - path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"), path( "message/", MessageUpdateView.as_view(), diff --git a/src/agenda_culturel/views/general_pages_views.py b/src/agenda_culturel/views/general_pages_views.py index d6ccbee..0619d0f 100644 --- a/src/agenda_culturel/views/general_pages_views.py +++ b/src/agenda_culturel/views/general_pages_views.py @@ -1,11 +1,26 @@ -from django.db.models import Q +import calendar as _calendar +from datetime import date, timedelta, datetime + +from django.contrib.auth.decorators import login_required, permission_required +from django.core.checks import messages +from django.core.paginator import PageNotAnInteger, EmptyPage +from django.db.models import Count, F +from django.db.models import Q, Min, Max, Avg, StdDev +from django.db.models.functions import TruncMonth, ExtractDay +from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse_lazy - -from ..models import RecurrentImport - from django.utils.translation import gettext_lazy as _ +from . import Median, week_view, PaginatorFilter +from ..filters import MessagesFilterAdmin +from ..models import RecurrentImport, Event, Message + + +def home(request, cat=None): + return week_view(request, home=True, cat=cat) + + def thank_you(request): return render(request, "agenda_culturel/thank_you.html") @@ -49,4 +64,190 @@ def import_requirements(request): "static_content": "import_requirements", "url_path": reverse_lazy("import_requirements"), } - return render(request, "agenda_culturel/page-single.html", context) \ No newline at end of file + return render(request, "agenda_culturel/page-single.html", context) + + +def statistics(request, pk=None): + if pk is not None: + rimport = RecurrentImport.objects.filter(pk=pk) + source = rimport.values("source").first()["source"] + qs = Event.objects.filter(import_sources__contains=[source]) + else: + rimport = None + qs = Event.objects + + stats = {} + stats_months = {} + first = {} + last = {} + + ev_published = qs.filter( + Q(status=Event.STATUS.PUBLISHED) + & ( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + ) + + for v in ["start_day", "created_date__date"]: + after = 24 + last[v] = ( + date.today() + if v == "created_date__date" + else date.today() + timedelta(weeks=after) + ) + last[v] = last[v].replace( + day=_calendar.monthrange(last[v].year, last[v].month)[1] + ) + + r = 8 * 30 + if v == "start_day": + r += after * 7 + first[v] = (last[v] - timedelta(days=r)).replace(day=1) + + ev_days = ev_published.annotate(day=F(v)).filter( + Q(day__lte=last[v]) & Q(day__gte=first[v]) + ) + + stats[v] = ev_days.values("day").annotate(total=Count("day")).order_by("day") + + stats_months[v] = ( + ev_days.annotate(month=TruncMonth("day")) + .values("month") + .annotate(total=Count("month")) + .order_by("month") + ) + + nb_by_city = ( + ev_published.annotate(city=F("exact_location__city")) + .filter(city__isnull=False) + .values("city") + .annotate(total=Count("city")) + .order_by("-total") + ) + + limit = datetime.now() + timedelta(days=-30) + + stat_qs = qs.filter(start_day__gte=F("created_date")).annotate( + foresight=ExtractDay(F("start_day") - F("created_date")) + ) + + statsa = stat_qs.filter().aggregate( + minimum=Min("foresight"), + maximum=Max("foresight"), + mean=Avg("foresight"), + median=Median("foresight"), + stdev=StdDev("foresight"), + ) + + statsm = stat_qs.filter(created_date__gte=limit).aggregate( + minimum=Min("foresight"), + maximum=Max("foresight"), + mean=Avg("foresight"), + median=Median("foresight"), + stdev=StdDev("foresight"), + ) + + stats_foresight = [ + [ + _(x), + round(statsa[x], 2) if statsa[x] is not None else "-", + round(statsm[x], 2) if statsm[x] is not None else "-", + ] + for x in statsa + ] + + context = { + "stats_by_startday": stats["start_day"], + "stats_by_creation": stats["created_date__date"], + "stats_months_by_startday": stats_months["start_day"], + "stats_months_by_creation": stats_months["created_date__date"], + "first_by_startday": first["start_day"], + "last_by_startday": last["start_day"], + "first_by_creation": first["created_date__date"], + "last_by_creation": last["created_date__date"], + "nb_by_city": nb_by_city, + "stats_foresight": stats_foresight, + "object": rimport.first() if rimport else None, + } + + if pk is None: + return render(request, "agenda_culturel/statistics.html", context) + else: + return render(request, "agenda_culturel/rimport-statistics.html", context) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_event") +def activite(request): + now = date.today() + + days = [now] + while len(days) < 7 or days[-1].weekday() != 0: + days.append(days[-1] + timedelta(days=-1)) + + weeks = [days[-1]] + for w in range(0, 8): + weeks.append(weeks[-1] + timedelta(days=-7)) + + daily_modifications = Event.get_count_modifications([(d, 1) for d in days]) + weekly_modifications = Event.get_count_modifications([(w, 7) for w in weeks]) + + return render( + request, + "agenda_culturel/page-activity.html", + { + "daily_modifications": daily_modifications, + "weekly_modifications": weekly_modifications, + }, + ) + + +@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") + ) + paginator = PaginatorFilter(filter, 10, request) + page = request.GET.get("page") + + nb_spams = Message.objects.filter(spam=True).count() + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/messages.html", + {"filter": filter, "nb_spams": nb_spams, "paginator_filter": response}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_message") +def delete_cm_spam(request): + if request.method == "POST": + Message.objects.filter(spam=True).delete() + + messages.success(request, _("Spam has been successfully deleted.")) + return HttpResponseRedirect(reverse_lazy("messages")) + else: + nb_msgs = Message.objects.values("spam").annotate(total=Count("spam")) + nb_total = sum([nb["total"] for nb in nb_msgs]) + nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]]) + cancel_url = reverse_lazy("messages") + return render( + request, + "agenda_culturel/delete_spams_confirm.html", + { + "nb_total": nb_total, + "nb_spams": nb_spams, + "cancel_url": cancel_url, + }, + ) diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 3799dfa..312cc50 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -1,4 +1,3 @@ -import calendar as _calendar import hashlib import logging from datetime import date, timedelta @@ -15,11 +14,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Aggregate, FloatField -from django.db.models import Avg, Max, Min -from django.db.models import Count, F, Func, OuterRef, Q, Subquery -from django.db.models.aggregates import StdDev -from django.db.models.functions import ExtractDay -from django.db.models.functions import TruncMonth +from django.db.models import F, Func, OuterRef, Q, Subquery from django.http import ( HttpResponse, HttpResponseForbidden, @@ -50,7 +45,6 @@ from ..celery import ( from ..filters import ( EventFilter, EventFilterAdmin, - MessagesFilterAdmin, SearchEventFilter, SimpleSearchEventFilter, ) @@ -148,10 +142,6 @@ to_be_translated = [ ] -def home(request, cat=None): - return week_view(request, home=True, cat=cat) - - def month_view(request, year=None, month=None, cat=None): now = date.today() if year is None and month is None: @@ -1011,32 +1001,6 @@ class MessageUpdateView( return kwargs -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_event") -def activite(request): - now = date.today() - - days = [now] - while len(days) < 7 or days[-1].weekday() != 0: - days.append(days[-1] + timedelta(days=-1)) - - weeks = [days[-1]] - for w in range(0, 8): - weeks.append(weeks[-1] + timedelta(days=-7)) - - daily_modifications = Event.get_count_modifications([(d, 1) for d in days]) - weekly_modifications = Event.get_count_modifications([(w, 7) for w in weeks]) - - return render( - request, - "agenda_culturel/page-activity.html", - { - "daily_modifications": daily_modifications, - "weekly_modifications": weekly_modifications, - }, - ) - - @login_required(login_url="/accounts/login/") @permission_required("agenda_culturel.view_event") def administration(request): @@ -1145,55 +1109,6 @@ 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") - ) - paginator = PaginatorFilter(filter, 10, request) - page = request.GET.get("page") - - nb_spams = Message.objects.filter(spam=True).count() - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/messages.html", - {"filter": filter, "nb_spams": nb_spams, "paginator_filter": response}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_message") -def delete_cm_spam(request): - if request.method == "POST": - Message.objects.filter(spam=True).delete() - - messages.success(request, _("Spam has been successfully deleted.")) - return HttpResponseRedirect(reverse_lazy("messages")) - else: - nb_msgs = Message.objects.values("spam").annotate(total=Count("spam")) - nb_total = sum([nb["total"] for nb in nb_msgs]) - nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]]) - cancel_url = reverse_lazy("messages") - return render( - request, - "agenda_culturel/delete_spams_confirm.html", - { - "nb_total": nb_total, - "nb_spams": nb_spams, - "cancel_url": cancel_url, - }, - ) - - def event_search(request, full=False): categories = None tags = None @@ -1289,117 +1204,6 @@ def event_search_full(request): return event_search(request, True) -def statistics(request, pk=None): - if pk is not None: - rimport = RecurrentImport.objects.filter(pk=pk) - source = rimport.values("source").first()["source"] - qs = Event.objects.filter(import_sources__contains=[source]) - else: - rimport = None - qs = Event.objects - - stats = {} - stats_months = {} - first = {} - last = {} - - ev_published = qs.filter( - Q(status=Event.STATUS.PUBLISHED) - & ( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - ) - - for v in ["start_day", "created_date__date"]: - after = 24 - last[v] = ( - date.today() - if v == "created_date__date" - else date.today() + timedelta(weeks=after) - ) - last[v] = last[v].replace( - day=_calendar.monthrange(last[v].year, last[v].month)[1] - ) - - r = 8 * 30 - if v == "start_day": - r += after * 7 - first[v] = (last[v] - timedelta(days=r)).replace(day=1) - - ev_days = ev_published.annotate(day=F(v)).filter( - Q(day__lte=last[v]) & Q(day__gte=first[v]) - ) - - stats[v] = ev_days.values("day").annotate(total=Count("day")).order_by("day") - - stats_months[v] = ( - ev_days.annotate(month=TruncMonth("day")) - .values("month") - .annotate(total=Count("month")) - .order_by("month") - ) - - nb_by_city = ( - ev_published.annotate(city=F("exact_location__city")) - .filter(city__isnull=False) - .values("city") - .annotate(total=Count("city")) - .order_by("-total") - ) - - limit = datetime.now() + timedelta(days=-30) - - stat_qs = qs.filter(start_day__gte=F("created_date")).annotate( - foresight=ExtractDay(F("start_day") - F("created_date")) - ) - - statsa = stat_qs.filter().aggregate( - minimum=Min("foresight"), - maximum=Max("foresight"), - mean=Avg("foresight"), - median=Median("foresight"), - stdev=StdDev("foresight"), - ) - - statsm = stat_qs.filter(created_date__gte=limit).aggregate( - minimum=Min("foresight"), - maximum=Max("foresight"), - mean=Avg("foresight"), - median=Median("foresight"), - stdev=StdDev("foresight"), - ) - - stats_foresight = [ - [ - _(x), - round(statsa[x], 2) if statsa[x] is not None else "-", - round(statsm[x], 2) if statsm[x] is not None else "-", - ] - for x in statsa - ] - - context = { - "stats_by_startday": stats["start_day"], - "stats_by_creation": stats["created_date__date"], - "stats_months_by_startday": stats_months["start_day"], - "stats_months_by_creation": stats_months["created_date__date"], - "first_by_startday": first["start_day"], - "last_by_startday": last["start_day"], - "first_by_creation": first["created_date__date"], - "last_by_creation": last["created_date__date"], - "nb_by_city": nb_by_city, - "stats_foresight": stats_foresight, - "object": rimport.first() if rimport else None, - } - - if pk is None: - return render(request, "agenda_culturel/statistics.html", context) - else: - return render(request, "agenda_culturel/rimport-statistics.html", context) - - def clear_cache(request): if request.method == "POST": cache.clear() From 001a1652bded45408638f1b10711234da73781e0 Mon Sep 17 00:00:00 2001 From: SebF Date: Mon, 14 Apr 2025 17:50:59 +0200 Subject: [PATCH 14/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20recherche?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 10 +- src/agenda_culturel/views/__init__.py | 1 + src/agenda_culturel/views/oldviews.py | 100 +------------------ src/agenda_culturel/views/search_views.py | 111 ++++++++++++++++++++++ 4 files changed, 119 insertions(+), 103 deletions(-) create mode 100644 src/agenda_culturel/views/search_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 66bd2c3..d5ea35d 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -90,6 +90,9 @@ from .views import ( UnknownPlaceAddView, UnknownPlacesListView, fix_unknown_places, + # Search + event_search, + event_search_full, # TODO pas encore trié week_view, month_view, @@ -107,8 +110,6 @@ from .views import ( StaticContentUpdateView, MessageCreateView, EventCreateView, - event_search, - event_search_full, update_from_source, change_status_event, EventDeleteView, @@ -354,6 +355,9 @@ urlpatterns = [ fix_unknown_places, name="fix_unknown_places", ), + # Search + path("rechercher", event_search, name="event_search"), + path("rechercher/complet/", event_search_full, name="event_search_full"), # TODO pas encore trié path("cat:/", home, name="home_category"), path( @@ -464,8 +468,6 @@ urlpatterns = [ StaticContentUpdateView.as_view(), name="edit_static_content", ), - path("rechercher", event_search, name="event_search"), - path("rechercher/complet/", event_search_full, name="event_search_full"), path("contact", MessageCreateView.as_view(), name="contact"), path( "message/", diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 48dd34b..25a6309 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -9,3 +9,4 @@ from .moderation_views import * from .organisations_views import * from .places_views import * from .tag_views import * +from .search_views import * diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 312cc50..4b76559 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -14,7 +14,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Aggregate, FloatField -from django.db.models import F, Func, OuterRef, Q, Subquery +from django.db.models import F, OuterRef, Q, Subquery from django.http import ( HttpResponse, HttpResponseForbidden, @@ -45,8 +45,6 @@ from ..celery import ( from ..filters import ( EventFilter, EventFilterAdmin, - SearchEventFilter, - SimpleSearchEventFilter, ) from ..forms import ( EventForm, @@ -69,7 +67,6 @@ from ..models import ( Place, RecurrentImport, StaticContent, - remove_accents, UserProfile, ) @@ -1109,101 +1106,6 @@ def recent(request): ) -def event_search(request, full=False): - categories = None - tags = None - places = None - organisations = None - rimports = None - - qs = get_event_qs(request).order_by("-start_day") - if not request.user.is_authenticated: - qs = qs.filter( - ( - Q(other_versions__isnull=True) - | Q(other_versions__representative=F("pk")) - | Q(other_versions__representative__isnull=True) - ) - ) - if full: - filter = SearchEventFilter( - request.GET, - queryset=qs, - request=request, - ) - else: - filter = SimpleSearchEventFilter( - request.GET, - queryset=qs, - request=request, - ) - if "q" in request.GET: - categories = Category.objects.filter(name__icontains=request.GET["q"]) - s_q = remove_accents(request.GET["q"].lower()) - tags = ( - Event.objects.extra( - where=["%s ILIKE ANY (tags)"], params=[request.GET["q"]] - ) - .annotate(arr_tags=Func(F("tags"), function="unnest")) - .values_list("arr_tags", flat=True) - .distinct() - ) - tags = [ - ( - t, - emoji.demojize(remove_accents(t).lower(), delimiters=("000", "")), - ) - for t in tags - ] - tags = [t for t in tags if s_q == t[1]] - tags.sort(key=lambda x: x[1]) - tags = [t[0] for t in tags] - places = Place.objects.filter( - Q(name__icontains=request.GET["q"]) - | Q(description__icontains=request.GET["q"]) - | Q(city__icontains=request.GET["q"]) - ) - organisations = Organisation.objects.filter( - Q(name__icontains=request.GET["q"]) - | Q(description__icontains=request.GET["q"]) - ) - if request.user.is_authenticated: - rimports = RecurrentImport.objects.filter( - name__icontains=request.GET["q"] - ) - - paginator = PaginatorFilter(filter, 10, request) - page = request.GET.get("page") - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/search.html", - { - "filter": filter, - "categories": categories, - "tags": tags, - "places": places, - "organisations": organisations, - "rimports": rimports, - "has_results": len(request.GET) != 0 - or (len(request.GET) > 1 and "page" in request.GET), - "paginator_filter": response, - "full": full, - }, - ) - - -def event_search_full(request): - return event_search(request, True) - - def clear_cache(request): if request.method == "POST": cache.clear() diff --git a/src/agenda_culturel/views/search_views.py b/src/agenda_culturel/views/search_views.py new file mode 100644 index 0000000..8bd38d8 --- /dev/null +++ b/src/agenda_culturel/views/search_views.py @@ -0,0 +1,111 @@ +import emoji +from django.core.paginator import PageNotAnInteger, EmptyPage +from django.shortcuts import render + +from . import PaginatorFilter +from ..filters import SearchEventFilter, SimpleSearchEventFilter +from ..models import ( + Category, + remove_accents, + Event, + Place, + Organisation, + RecurrentImport, +) +from ..views import get_event_qs +from django.db.models import Q, F, Func + + +def event_search(request, full=False): + categories = None + tags = None + places = None + organisations = None + rimports = None + + qs = get_event_qs(request).order_by("-start_day") + if not request.user.is_authenticated: + qs = qs.filter( + ( + Q(other_versions__isnull=True) + | Q(other_versions__representative=F("pk")) + | Q(other_versions__representative__isnull=True) + ) + ) + if full: + filter = SearchEventFilter( + request.GET, + queryset=qs, + request=request, + ) + else: + filter = SimpleSearchEventFilter( + request.GET, + queryset=qs, + request=request, + ) + if "q" in request.GET: + categories = Category.objects.filter(name__icontains=request.GET["q"]) + s_q = remove_accents(request.GET["q"].lower()) + tags = ( + Event.objects.extra( + where=["%s ILIKE ANY (tags)"], params=[request.GET["q"]] + ) + .annotate(arr_tags=Func(F("tags"), function="unnest")) + .values_list("arr_tags", flat=True) + .distinct() + ) + tags = [ + ( + t, + emoji.demojize(remove_accents(t).lower(), delimiters=("000", "")), + ) + for t in tags + ] + tags = [t for t in tags if s_q == t[1]] + tags.sort(key=lambda x: x[1]) + tags = [t[0] for t in tags] + places = Place.objects.filter( + Q(name__icontains=request.GET["q"]) + | Q(description__icontains=request.GET["q"]) + | Q(city__icontains=request.GET["q"]) + ) + organisations = Organisation.objects.filter( + Q(name__icontains=request.GET["q"]) + | Q(description__icontains=request.GET["q"]) + ) + if request.user.is_authenticated: + rimports = RecurrentImport.objects.filter( + name__icontains=request.GET["q"] + ) + + paginator = PaginatorFilter(filter, 10, request) + page = request.GET.get("page") + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/search.html", + { + "filter": filter, + "categories": categories, + "tags": tags, + "places": places, + "organisations": organisations, + "rimports": rimports, + "has_results": len(request.GET) != 0 + or (len(request.GET) > 1 and "page" in request.GET), + "paginator_filter": response, + "full": full, + }, + ) + + +def event_search_full(request): + return event_search(request, True) From 94950e148f2df441cba1dcbdbbf62a18f3710a98 Mon Sep 17 00:00:00 2001 From: SebF Date: Mon, 14 Apr 2025 18:01:22 +0200 Subject: [PATCH 15/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 91 +++++------ src/agenda_culturel/views/__init__.py | 1 + .../views/general_pages_views.py | 57 +------ src/agenda_culturel/views/message_views.py | 146 ++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 82 ---------- 5 files changed, 196 insertions(+), 181 deletions(-) create mode 100644 src/agenda_culturel/views/message_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index d5ea35d..2d57292 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -36,7 +36,6 @@ from .views import ( # General pages about, activite, - delete_cm_spam, home, import_requirements, mentions_legales, @@ -48,7 +47,6 @@ from .views import ( add_import, cancel_import, update_orphan_events, - view_messages, # Import récurrent RecurrentImportCreateView, RecurrentImportDeleteView, @@ -58,6 +56,12 @@ from .views import ( run_all_rimports, run_rimport, view_rimport, + # Messages + delete_cm_spam, + MessageCreateView, + MessageDeleteView, + MessageUpdateView, + view_messages, # Moderation EventModerateView, moderate_event_next, @@ -69,15 +73,6 @@ from .views import ( OrganisationDetailViewPast, OrganisationListView, OrganisationUpdateView, - # Tags - view_tag, - view_tag_past, - TagUpdateView, - tag_list, - TagDeleteView, - rename_tag, - delete_tag, - TagCreateView, # Places PlaceCreateView, PlaceDeleteView, @@ -93,6 +88,15 @@ from .views import ( # Search event_search, event_search_full, + # Tags + view_tag, + view_tag_past, + TagUpdateView, + tag_list, + TagDeleteView, + rename_tag, + delete_tag, + TagCreateView, # TODO pas encore trié week_view, month_view, @@ -103,12 +107,10 @@ from .views import ( administration, clear_cache, export_event_ical, - MessageDeleteView, EventDetailView, EventUpdateView, StaticContentCreateView, StaticContentUpdateView, - MessageCreateView, EventCreateView, update_from_source, change_status_event, @@ -116,7 +118,6 @@ from .views import ( import_event_proxy, import_from_url, import_from_urls, - MessageUpdateView, UserProfileUpdateView, ) @@ -191,14 +192,12 @@ urlpatterns = [ # General pages path("a-propos", about, name="about"), path("activite/", activite, name="activite"), - path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"), path("", home, name="home"), path("besoin-pour-import", import_requirements, name="import_requirements"), path("mentions-legales", mentions_legales, name="mentions_legales"), path("regles-de-moderation", moderation_rules, name="moderation_rules"), path("statistiques", statistics, name="statistics"), path("merci", thank_you, name="thank_you"), - path("messages", view_messages, name="messages"), # Import batch path("imports/", imports, name="imports"), path("imports/add", add_import, name="add_import"), @@ -235,6 +234,25 @@ urlpatterns = [ ), path("rimports//run", run_rimport, name="run_rimport"), path("rimports//view", view_rimport, name="view_rimport"), + # Messages + path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"), + path( + "event//message", + MessageCreateView.as_view(), + name="message_for_event", + ), + path("contact", MessageCreateView.as_view(), name="contact"), + path( + "message//delete", + MessageDeleteView.as_view(), + name="delete_message", + ), + path( + "message/", + MessageUpdateView.as_view(), + name="message", + ), + path("messages", view_messages, name="messages"), # Moderation path("moderate", EventModerateView.as_view(), name="moderate"), path( @@ -358,6 +376,19 @@ urlpatterns = [ # Search path("rechercher", event_search, name="event_search"), path("rechercher/complet/", event_search_full, name="event_search_full"), + # Tags + path("tag//", view_tag, name="view_tag"), + path("tag//past", view_tag_past, name="view_tag_past"), + path("tag//edit", TagUpdateView.as_view(), name="edit_tag"), + path("tags/", tag_list, name="view_all_tags"), + path( + "tag//delete", + TagDeleteView.as_view(), + name="delete_object_tag", + ), + path("tag//rename", rename_tag, name="rename_tag"), + path("tag//delete", delete_tag, name="delete_tag"), + path("tags/add", TagCreateView.as_view(), name="add_tag"), # TODO pas encore trié path("cat:/", home, name="home_category"), path( @@ -400,19 +431,7 @@ urlpatterns = [ ), path("cette-semaine/", week_view, name="cette_semaine"), path("ce-mois-ci", month_view, name="ce_mois_ci"), - path("tag//", view_tag, name="view_tag"), path("tag//ical", export_ical, name="export_ical_tag"), - path("tag//past", view_tag_past, name="view_tag_past"), - path("tags/", tag_list, name="view_all_tags"), - path("tag//edit", TagUpdateView.as_view(), name="edit_tag"), - path( - "tag//delete", - TagDeleteView.as_view(), - name="delete_object_tag", - ), - path("tag//rename", rename_tag, name="rename_tag"), - path("tag//delete", delete_tag, name="delete_tag"), - path("tags/add", TagCreateView.as_view(), name="add_tag"), path("recent/", recent, name="recent"), path("administration/", administration, name="administration"), path( @@ -435,11 +454,6 @@ urlpatterns = [ EventUpdateView.as_view(), name="clone_edit", ), - path( - "event//message", - MessageCreateView.as_view(), - name="message_for_event", - ), path( "event//update-from-source", update_from_source, @@ -468,17 +482,6 @@ urlpatterns = [ StaticContentUpdateView.as_view(), name="edit_static_content", ), - path("contact", MessageCreateView.as_view(), name="contact"), - path( - "message/", - MessageUpdateView.as_view(), - name="message", - ), - path( - "message//delete", - MessageDeleteView.as_view(), - name="delete_message", - ), path("rimports//stats", statistics, name="stats_rimport"), path( "organisme//ical", diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 25a6309..5374993 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -5,6 +5,7 @@ from .errors import * from .general_pages_views import * from .import_batch_views import * from .import_recurrent_views import * +from .message_views import * from .moderation_views import * from .organisations_views import * from .places_views import * diff --git a/src/agenda_culturel/views/general_pages_views.py b/src/agenda_culturel/views/general_pages_views.py index 0619d0f..685954a 100644 --- a/src/agenda_culturel/views/general_pages_views.py +++ b/src/agenda_culturel/views/general_pages_views.py @@ -2,19 +2,15 @@ import calendar as _calendar from datetime import date, timedelta, datetime from django.contrib.auth.decorators import login_required, permission_required -from django.core.checks import messages -from django.core.paginator import PageNotAnInteger, EmptyPage from django.db.models import Count, F from django.db.models import Q, Min, Max, Avg, StdDev from django.db.models.functions import TruncMonth, ExtractDay -from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from . import Median, week_view, PaginatorFilter -from ..filters import MessagesFilterAdmin -from ..models import RecurrentImport, Event, Message +from . import Median, week_view +from ..models import RecurrentImport, Event def home(request, cat=None): @@ -202,52 +198,3 @@ def activite(request): "weekly_modifications": weekly_modifications, }, ) - - -@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") - ) - paginator = PaginatorFilter(filter, 10, request) - page = request.GET.get("page") - - nb_spams = Message.objects.filter(spam=True).count() - - try: - response = paginator.page(page) - except PageNotAnInteger: - response = paginator.page(1) - except EmptyPage: - response = paginator.page(paginator.num_pages) - - return render( - request, - "agenda_culturel/messages.html", - {"filter": filter, "nb_spams": nb_spams, "paginator_filter": response}, - ) - - -@login_required(login_url="/accounts/login/") -@permission_required("agenda_culturel.view_message") -def delete_cm_spam(request): - if request.method == "POST": - Message.objects.filter(spam=True).delete() - - messages.success(request, _("Spam has been successfully deleted.")) - return HttpResponseRedirect(reverse_lazy("messages")) - else: - nb_msgs = Message.objects.values("spam").annotate(total=Count("spam")) - nb_total = sum([nb["total"] for nb in nb_msgs]) - nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]]) - cancel_url = reverse_lazy("messages") - return render( - request, - "agenda_culturel/delete_spams_confirm.html", - { - "nb_total": nb_total, - "nb_spams": nb_spams, - "cancel_url": cancel_url, - }, - ) diff --git a/src/agenda_culturel/views/message_views.py b/src/agenda_culturel/views/message_views.py new file mode 100644 index 0000000..fce5c24 --- /dev/null +++ b/src/agenda_culturel/views/message_views.py @@ -0,0 +1,146 @@ +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.core.checks import messages +from django.core.paginator import PageNotAnInteger, EmptyPage +from django.db.models import Count +from django.http import HttpResponseRedirect +from django.shortcuts import render, get_object_or_404 +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.views.generic import DeleteView, UpdateView, CreateView +from honeypot.decorators import check_honeypot + +from . import PaginatorFilter +from ..filters import MessagesFilterAdmin +from ..forms import MessageForm +from ..models import Event, Message + + +@method_decorator(check_honeypot, name="post") +class MessageCreateView(SuccessMessageMixin, CreateView): + model = Message + template_name = "agenda_culturel/message_create_form.html" + form_class = MessageForm + + success_url = reverse_lazy("home") + success_message = _("Your message has been sent successfully.") + + def __init__(self, *args, **kwargs): + self.event = None + super().__init__(*args, **kwargs) + + def get_form(self, form_class=None): + if form_class is None: + form_class = self.get_form_class() + 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 + success_message = _("The contact message has been successfully deleted.") + success_url = reverse_lazy("messages") + + +class MessageUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = Message + permission_required = "agenda_culturel.change_message" + template_name = "agenda_culturel/message_moderation_form.html" + fields = ("spam", "closed", "comments") + + success_message = _( + "The contact message properties has been successfully modified." + ) + + success_url = reverse_lazy("messages") + + def get_form_kwargs(self): + """Return the keyword arguments for instantiating the form.""" + kwargs = super().get_form_kwargs() + if hasattr(self, "object"): + kwargs.update({"instance": self.object}) + return kwargs + + +@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") + ) + paginator = PaginatorFilter(filter, 10, request) + page = request.GET.get("page") + + nb_spams = Message.objects.filter(spam=True).count() + + try: + response = paginator.page(page) + except PageNotAnInteger: + response = paginator.page(1) + except EmptyPage: + response = paginator.page(paginator.num_pages) + + return render( + request, + "agenda_culturel/messages.html", + {"filter": filter, "nb_spams": nb_spams, "paginator_filter": response}, + ) + + +@login_required(login_url="/accounts/login/") +@permission_required("agenda_culturel.view_message") +def delete_cm_spam(request): + if request.method == "POST": + Message.objects.filter(spam=True).delete() + + messages.success(request, _("Spam has been successfully deleted.")) + return HttpResponseRedirect(reverse_lazy("messages")) + else: + nb_msgs = Message.objects.values("spam").annotate(total=Count("spam")) + nb_total = sum([nb["total"] for nb in nb_msgs]) + nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]]) + cancel_url = reverse_lazy("messages") + return render( + request, + "agenda_culturel/delete_spams_confirm.html", + { + "nb_total": nb_total, + "nb_spams": nb_spams, + "cancel_url": cancel_url, + }, + ) diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 4b76559..f4efc10 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -22,7 +22,6 @@ from django.http import ( ) from django.shortcuts import get_object_or_404, render from django.urls import reverse, reverse_lazy -from django.utils.decorators import method_decorator from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.timezone import datetime @@ -34,7 +33,6 @@ from django.views.generic.edit import ( ModelFormMixin, UpdateView, ) -from honeypot.decorators import check_honeypot from .utils import get_event_qs from ..calendar import CalendarList, CalendarMonth, CalendarWeek @@ -50,7 +48,6 @@ from ..forms import ( EventForm, EventFormWithContact, MessageEventForm, - MessageForm, SimpleContactForm, URLSubmissionFormSet, URLSubmissionFormWithContact, @@ -919,85 +916,6 @@ def export_ical(request, cat=None, tag=None, organisation_pk=None, place_pk=None 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 - - 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 - success_message = _("The contact message has been successfully deleted.") - success_url = reverse_lazy("messages") - - -class MessageUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = Message - permission_required = "agenda_culturel.change_message" - template_name = "agenda_culturel/message_moderation_form.html" - fields = ("spam", "closed", "comments") - - success_message = _( - "The contact message properties has been successfully modified." - ) - - success_url = reverse_lazy("messages") - - def get_form_kwargs(self): - """Return the keyword arguments for instantiating the form.""" - kwargs = super().get_form_kwargs() - if hasattr(self, "object"): - kwargs.update({"instance": self.object}) - return kwargs - - @login_required(login_url="/accounts/login/") @permission_required("agenda_culturel.view_event") def administration(request): From 0c7280f50ef926768ad58337d4bb9384cfa3df2c Mon Sep 17 00:00:00 2001 From: SebF Date: Mon, 14 Apr 2025 18:07:05 +0200 Subject: [PATCH 16/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 34 +++---- src/agenda_culturel/views/__init__.py | 1 + src/agenda_culturel/views/export_views.py | 103 ++++++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 95 -------------------- 4 files changed, 122 insertions(+), 111 deletions(-) create mode 100644 src/agenda_culturel/views/export_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 2d57292..3ea4e02 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -33,6 +33,9 @@ from .views import ( merge_duplicate, set_duplicate, update_duplicate_event, + # Export + export_event_ical, + export_ical, # General pages about, activite, @@ -102,11 +105,9 @@ from .views import ( month_view, day_view, upcoming_events, - export_ical, recent, administration, clear_cache, - export_event_ical, EventDetailView, EventUpdateView, StaticContentCreateView, @@ -187,6 +188,21 @@ urlpatterns = [ DuplicatedEventsDetailView.as_view(), name="view_duplicate", ), + # Export + path( + "event/////ical", + export_event_ical, + name="export_event_ical", + ), + path("cat:/ical", export_ical, name="export_ical_category"), + path("ical", export_ical, name="export_ical"), + path( + "organisme//ical", + export_ical, + name="export_ical_organisation", + ), + path("place//ical", export_ical, name="export_ical_place"), + path("tag//ical", export_ical, name="export_ical_tag"), path("duplicates/", duplicates, name="duplicates"), path("duplicates//fix", fix_duplicate, name="fix_duplicate"), # General pages @@ -416,7 +432,6 @@ urlpatterns = [ name="a_venir_jour_category", ), path("cat:/cette-semaine/", week_view, name="cette_semaine_category"), - path("cat:/ical", export_ical, name="export_ical_category"), path("cat:/ce-mois-ci", month_view, name="ce_mois_ci_category"), path("semaine///", week_view, name="week_view"), path("mois///", month_view, name="month_view"), @@ -431,7 +446,6 @@ urlpatterns = [ ), path("cette-semaine/", week_view, name="cette_semaine"), path("ce-mois-ci", month_view, name="ce_mois_ci"), - path("tag//ical", export_ical, name="export_ical_tag"), path("recent/", recent, name="recent"), path("administration/", administration, name="administration"), path( @@ -483,18 +497,6 @@ urlpatterns = [ name="edit_static_content", ), path("rimports//stats", statistics, name="stats_rimport"), - path( - "organisme//ical", - export_ical, - name="export_ical_organisation", - ), - path("place//ical", export_ical, name="export_ical_place"), - path( - "event/////ical", - export_event_ical, - name="export_event_ical", - ), - path("ical", export_ical, name="export_ical"), re_path(r"^robots\.txt", include("robots.urls")), path("__debug__/", include("debug_toolbar.urls")), path("ckeditor5/", include("django_ckeditor_5.urls")), diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 5374993..9c383f5 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -2,6 +2,7 @@ from .oldviews import * from .categorisation_rules_view import * from .event_duplicate_views import * from .errors import * +from .export_views import * from .general_pages_views import * from .import_batch_views import * from .import_recurrent_views import * diff --git a/src/agenda_culturel/views/export_views.py b/src/agenda_culturel/views/export_views.py new file mode 100644 index 0000000..42600c2 --- /dev/null +++ b/src/agenda_culturel/views/export_views.py @@ -0,0 +1,103 @@ +import hashlib +from datetime import timedelta, date + +import emoji +from django.core.cache import cache + +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404 + +from . import get_event_qs +from ..calendar import CalendarList +from ..filters import EventFilter +from ..models import Event, Category, Place, Organisation + + +def export_event_ical(request, year, month, day, pk): + event = get_object_or_404(Event, pk=pk) + event = event.get_recurrence_at_date(year, month, day) + + events = list() + events.append(event) + + cal = Event.export_to_ics(events, request) + + response = HttpResponse(content_type="text/calendar") + response.content = cal.to_ical().decode("utf-8").replace("\r\n", "\n") + response["Content-Disposition"] = "attachment; filename={0}{1}".format( + event.title.replace("\n", " ").replace("\r", "")[0:32], ".ics" + ) + + return response + + +def export_ical(request, cat=None, tag=None, organisation_pk=None, place_pk=None): + now = date.today() + + qs = get_event_qs(request) + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + + if place_pk is not None: + qs = qs.filter(exact_location=place_pk) + if organisation_pk is not None: + qs = qs.filter(organisers__in=[organisation_pk]) + if tag is not None: + qs = qs.filter(tags__in=[tag]) + + request = EventFilter.set_default_values(request) + filter = EventFilter(request.GET, queryset=qs, request=request) + + if filter.has_category_parameters(): + return HttpResponseRedirect(filter.get_new_url()) + + id_cache = hashlib.md5( + ( + filter.get_url() + + "-" + + str(tag) + + "-" + + str(cat) + + "-" + + str(organisation_pk) + + "-" + + str(place_pk) + ).encode("utf8") + ).hexdigest() + ical = cache.get(id_cache) + if not ical: + calendar = CalendarList( + now + timedelta(days=-7), now + timedelta(days=+60), filter + ) + ical = calendar.export_to_ics(request) + cache.set(id_cache, ical, 3600) # 1 heure + + response = HttpResponse(content_type="text/calendar") + response.content = ical.to_ical().decode("utf-8").replace("\r\n", "\n") + extra = filter.to_str(" ") + if extra is None: + extra = "" + if category is not None: + extra += " " + str(category) + if place_pk is not None: + extra += ( + " @ " + Place.objects.filter(pk=place_pk).values("name").first()["name"] + ) + if organisation_pk is not None: + extra += ( + " - " + + Organisation.objects.filter(pk=organisation_pk) + .values("name") + .first()["name"] + ) + if tag is not None: + extra += " - " + emoji.replace_emoji(tag, replace="") + + response["Content-Disposition"] = "attachment; filename={0}{1}{2}".format( + "Pommes de lune", extra, ".ics" + ) + + return response diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index f4efc10..774fdf4 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -1,8 +1,6 @@ -import hashlib import logging from datetime import date, timedelta -import emoji from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.mixins import ( @@ -16,7 +14,6 @@ from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Aggregate, FloatField from django.db.models import F, OuterRef, Q, Subquery from django.http import ( - HttpResponse, HttpResponseForbidden, HttpResponseRedirect, ) @@ -60,8 +57,6 @@ from ..models import ( DuplicatedEvents, Event, Message, - Organisation, - Place, RecurrentImport, StaticContent, UserProfile, @@ -826,96 +821,6 @@ def import_from_url(request): ) -def export_event_ical(request, year, month, day, pk): - event = get_object_or_404(Event, pk=pk) - event = event.get_recurrence_at_date(year, month, day) - - events = list() - events.append(event) - - cal = Event.export_to_ics(events, request) - - response = HttpResponse(content_type="text/calendar") - response.content = cal.to_ical().decode("utf-8").replace("\r\n", "\n") - response["Content-Disposition"] = "attachment; filename={0}{1}".format( - event.title.replace("\n", " ").replace("\r", "")[0:32], ".ics" - ) - - return response - - -def export_ical(request, cat=None, tag=None, organisation_pk=None, place_pk=None): - now = date.today() - - qs = get_event_qs(request) - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - - if place_pk is not None: - qs = qs.filter(exact_location=place_pk) - if organisation_pk is not None: - qs = qs.filter(organisers__in=[organisation_pk]) - if tag is not None: - qs = qs.filter(tags__in=[tag]) - - request = EventFilter.set_default_values(request) - filter = EventFilter(request.GET, queryset=qs, request=request) - - if filter.has_category_parameters(): - return HttpResponseRedirect(filter.get_new_url()) - - id_cache = hashlib.md5( - ( - filter.get_url() - + "-" - + str(tag) - + "-" - + str(cat) - + "-" - + str(organisation_pk) - + "-" - + str(place_pk) - ).encode("utf8") - ).hexdigest() - ical = cache.get(id_cache) - if not ical: - calendar = CalendarList( - now + timedelta(days=-7), now + timedelta(days=+60), filter - ) - ical = calendar.export_to_ics(request) - cache.set(id_cache, ical, 3600) # 1 heure - - response = HttpResponse(content_type="text/calendar") - response.content = ical.to_ical().decode("utf-8").replace("\r\n", "\n") - extra = filter.to_str(" ") - if extra is None: - extra = "" - if category is not None: - extra += " " + str(category) - if place_pk is not None: - extra += ( - " @ " + Place.objects.filter(pk=place_pk).values("name").first()["name"] - ) - if organisation_pk is not None: - extra += ( - " - " - + Organisation.objects.filter(pk=organisation_pk) - .values("name") - .first()["name"] - ) - if tag is not None: - extra += " - " + emoji.replace_emoji(tag, replace="") - - response["Content-Disposition"] = "attachment; filename={0}{1}{2}".format( - "Pommes de lune", extra, ".ics" - ) - - return response - - @login_required(login_url="/accounts/login/") @permission_required("agenda_culturel.view_event") def administration(request): From a8b10f38995275d9f02485d47f02736d86f4a56e Mon Sep 17 00:00:00 2001 From: SebF Date: Mon, 14 Apr 2025 18:14:51 +0200 Subject: [PATCH 17/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20d=E2=80=99?= =?UTF-8?q?affichage=20d=E2=80=99=C3=A9v=C3=A9nements=20en=20calendrier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 62 +++++---- src/agenda_culturel/views/__init__.py | 2 + src/agenda_culturel/views/calendar_views.py | 143 ++++++++++++++++++++ src/agenda_culturel/views/oldviews.py | 132 +----------------- 4 files changed, 178 insertions(+), 161 deletions(-) create mode 100644 src/agenda_culturel/views/calendar_views.py diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 3ea4e02..d498780 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -17,6 +17,10 @@ from .sitemaps import ( ) from .models import Event, Place, Organisation, Category from .views import ( + # Calendar + day_view, + month_view, + week_view, # Categorisation rules CategorisationRuleCreateView, CategorisationRuleDeleteView, @@ -101,9 +105,6 @@ from .views import ( delete_tag, TagCreateView, # TODO pas encore trié - week_view, - month_view, - day_view, upcoming_events, recent, administration, @@ -150,6 +151,34 @@ sitemaps = { } urlpatterns = [ + # Calendar + path( + "cat:/jour////", + day_view, + name="day_view_category", + ), + path("cat:/jour/", day_view, name="day_view_category_when"), + path("cat:/aujourdhui/", day_view, name="aujourdhui_category"), + path("jour////", day_view, name="day_view"), + path("jour/", day_view, name="day_view_when"), + path("aujourdhui/", day_view, name="aujourdhui"), + path( + "cat:/mois///", + month_view, + name="month_view_category", + ), + path("cat:/ce-mois-ci", month_view, name="ce_mois_ci_category"), + path("mois///", month_view, name="month_view"), + path("ce-mois-ci", month_view, name="ce_mois_ci"), + path( + "cat:/semaine///", + week_view, + name="week_view_category", + ), + path("cat:/cette-semaine/", week_view, name="cette_semaine_category"), + path("cat:/cette-semaine/", week_view, name="cette_semaine_category"), + path("semaine///", week_view, name="week_view"), + path("cette-semaine/", week_view, name="cette_semaine"), # Categorisation rules path( "catrules/add", @@ -407,45 +436,18 @@ urlpatterns = [ path("tags/add", TagCreateView.as_view(), name="add_tag"), # TODO pas encore trié path("cat:/", home, name="home_category"), - path( - "cat:/semaine///", - week_view, - name="week_view_category", - ), - path("cat:/cette-semaine/", week_view, name="cette_semaine_category"), - path( - "cat:/mois///", - month_view, - name="month_view_category", - ), - path( - "cat:/jour////", - day_view, - name="day_view_category", - ), - path("cat:/jour/", day_view, name="day_view_category_when"), path("cat:/a-venir/", upcoming_events, name="a_venir_category"), - path("cat:/aujourdhui/", day_view, name="aujourdhui_category"), path( "cat:/a-venir////", upcoming_events, name="a_venir_jour_category", ), - path("cat:/cette-semaine/", week_view, name="cette_semaine_category"), - path("cat:/ce-mois-ci", month_view, name="ce_mois_ci_category"), - path("semaine///", week_view, name="week_view"), - path("mois///", month_view, name="month_view"), - path("jour////", day_view, name="day_view"), - path("jour/", day_view, name="day_view_when"), - path("aujourdhui/", day_view, name="aujourdhui"), path("a-venir/", upcoming_events, name="a_venir"), path( "a-venir////", upcoming_events, name="a_venir_jour", ), - path("cette-semaine/", week_view, name="cette_semaine"), - path("ce-mois-ci", month_view, name="ce_mois_ci"), path("recent/", recent, name="recent"), path("administration/", administration, name="administration"), path( diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 9c383f5..1fbeca5 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -1,4 +1,6 @@ from .oldviews import * + +from .calendar_views import * from .categorisation_rules_view import * from .event_duplicate_views import * from .errors import * diff --git a/src/agenda_culturel/views/calendar_views.py b/src/agenda_culturel/views/calendar_views.py new file mode 100644 index 0000000..3943a31 --- /dev/null +++ b/src/agenda_culturel/views/calendar_views.py @@ -0,0 +1,143 @@ +from datetime import date + +from django.http import ( + HttpResponseRedirect, +) +from django.shortcuts import render +from django.urls import reverse_lazy +from django.utils.timezone import datetime + +from . import upcoming_events +from .utils import get_event_qs +from ..calendar import CalendarMonth, CalendarWeek +from ..filters import EventFilter +from ..models import Category + + +def day_view(request, year=None, month=None, day=None, cat=None): + if year is None or month is None or day is None: + if "when" in request.POST: + when = datetime.strptime(request.POST["when"], "%Y-%m-%d") + year = when.year + month = when.month + day = when.day + + request = EventFilter.set_default_values(request) + qs = get_event_qs(request).select_related("exact_location") + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + filter = EventFilter(request.GET, qs, request=request) + + return HttpResponseRedirect( + reverse_lazy("day_view", args=[year, month, day]) + + "?" + + filter.get_url() + ) + + return upcoming_events(request, year, month, day, 0, cat) + + +def month_view(request, year=None, month=None, cat=None): + now = date.today() + if year is None and month is None: + day = now.day + else: + day = None + if year is None: + year = now.year + if month is None: + month = now.month + + request = EventFilter.set_default_values(request) + qs = get_event_qs(request).only( + "title", + "start_day", + "start_time", + "category", + "other_versions", + "recurrences", + "end_day", + "end_time", + "uuids", + "status", + "tags", + ) + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + + filter = EventFilter(request.GET, qs, request=request) + + if filter.has_category_parameters(): + return HttpResponseRedirect(filter.get_new_url()) + + cmonth = CalendarMonth(year, month, filter, day=day) + + context = { + "calendar": cmonth, + "this_month": day is not None, + "filter": filter, + "category": category, + "init_date": now if cmonth.today_in_calendar() else cmonth.firstdate, + } + return render(request, "agenda_culturel/page-month.html", context) + + +def week_view(request, year=None, week=None, home=False, cat=None): + now = date.today() + if year is None: + year = now.isocalendar()[0] + if week is None: + week = now.isocalendar()[1] + + request = EventFilter.set_default_values(request) + qs = ( + get_event_qs(request) + .select_related("exact_location") + .only( + "title", + "start_day", + "start_time", + "category", + "other_versions", + "recurrences", + "end_day", + "end_time", + "uuids", + "status", + "tags", + "local_image", + "image", + "image_alt", + "exact_location", + "description", + ) + ) + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + filter = EventFilter(request.GET, qs, request=request) + + if filter.has_category_parameters(): + return HttpResponseRedirect(filter.get_new_url()) + + cweek = CalendarWeek(year, week, filter) + + context = { + "year": year, + "week": week, + "calendar": cweek, + "filter": filter, + "category": category, + "init_date": now if cweek.today_in_calendar() else cweek.firstdate, + } + if home: + context["home"] = 1 + return render(request, "agenda_culturel/page-week.html", context) diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 774fdf4..321c5f9 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -21,7 +21,6 @@ from django.shortcuts import get_object_or_404, render from django.urls import reverse, reverse_lazy from django.utils.html import escape from django.utils.safestring import mark_safe -from django.utils.timezone import datetime from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView from django.views.generic.edit import ( @@ -32,7 +31,7 @@ from django.views.generic.edit import ( ) from .utils import get_event_qs -from ..calendar import CalendarList, CalendarMonth, CalendarWeek +from ..calendar import CalendarList from ..celery import ( import_events_from_url, import_events_from_urls, @@ -131,135 +130,6 @@ to_be_translated = [ ] -def month_view(request, year=None, month=None, cat=None): - now = date.today() - if year is None and month is None: - day = now.day - else: - day = None - if year is None: - year = now.year - if month is None: - month = now.month - - request = EventFilter.set_default_values(request) - qs = get_event_qs(request).only( - "title", - "start_day", - "start_time", - "category", - "other_versions", - "recurrences", - "end_day", - "end_time", - "uuids", - "status", - "tags", - ) - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - - filter = EventFilter(request.GET, qs, request=request) - - if filter.has_category_parameters(): - return HttpResponseRedirect(filter.get_new_url()) - - cmonth = CalendarMonth(year, month, filter, day=day) - - context = { - "calendar": cmonth, - "this_month": day is not None, - "filter": filter, - "category": category, - "init_date": now if cmonth.today_in_calendar() else cmonth.firstdate, - } - return render(request, "agenda_culturel/page-month.html", context) - - -def week_view(request, year=None, week=None, home=False, cat=None): - now = date.today() - if year is None: - year = now.isocalendar()[0] - if week is None: - week = now.isocalendar()[1] - - request = EventFilter.set_default_values(request) - qs = ( - get_event_qs(request) - .select_related("exact_location") - .only( - "title", - "start_day", - "start_time", - "category", - "other_versions", - "recurrences", - "end_day", - "end_time", - "uuids", - "status", - "tags", - "local_image", - "image", - "image_alt", - "exact_location", - "description", - ) - ) - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - filter = EventFilter(request.GET, qs, request=request) - - if filter.has_category_parameters(): - return HttpResponseRedirect(filter.get_new_url()) - - cweek = CalendarWeek(year, week, filter) - - context = { - "year": year, - "week": week, - "calendar": cweek, - "filter": filter, - "category": category, - "init_date": now if cweek.today_in_calendar() else cweek.firstdate, - } - if home: - context["home"] = 1 - return render(request, "agenda_culturel/page-week.html", context) - - -def day_view(request, year=None, month=None, day=None, cat=None): - if year is None or month is None or day is None: - if "when" in request.POST: - when = datetime.strptime(request.POST["when"], "%Y-%m-%d") - year = when.year - month = when.month - day = when.day - - request = EventFilter.set_default_values(request) - qs = get_event_qs(request).select_related("exact_location") - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - filter = EventFilter(request.GET, qs, request=request) - - return HttpResponseRedirect( - reverse_lazy("day_view", args=[year, month, day]) - + "?" - + filter.get_url() - ) - - return upcoming_events(request, year, month, day, 0, cat) - - def upcoming_events(request, year=None, month=None, day=None, neighsize=1, cat=None): now = date.today() if year is None: From 7f7f46a9bbfc6661265cdff68f4fa511d8a51bea Mon Sep 17 00:00:00 2001 From: SebF Date: Tue, 15 Apr 2025 10:38:39 +0200 Subject: [PATCH 18/20] ajout de pages aux vues generales --- src/agenda_culturel/urls.py | 62 ++-- src/agenda_culturel/views/calendar_views.py | 51 +++- .../views/general_pages_views.py | 154 +++++++++- src/agenda_culturel/views/oldviews.py | 264 +----------------- src/agenda_culturel/views/utils.py | 60 +++- 5 files changed, 291 insertions(+), 300 deletions(-) diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index d498780..0e26102 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -20,6 +20,7 @@ from .views import ( # Calendar day_view, month_view, + upcoming_events, week_view, # Categorisation rules CategorisationRuleCreateView, @@ -42,12 +43,17 @@ from .views import ( export_ical, # General pages about, + administration, activite, + clear_cache, home, import_requirements, mentions_legales, moderation_rules, + StaticContentCreateView, + StaticContentUpdateView, statistics, + UserProfileUpdateView, thank_you, # Import batch imports, @@ -105,14 +111,9 @@ from .views import ( delete_tag, TagCreateView, # TODO pas encore trié - upcoming_events, recent, - administration, - clear_cache, EventDetailView, EventUpdateView, - StaticContentCreateView, - StaticContentUpdateView, EventCreateView, update_from_source, change_status_event, @@ -120,7 +121,6 @@ from .views import ( import_event_proxy, import_from_url, import_from_urls, - UserProfileUpdateView, ) event_dict = { @@ -170,6 +170,18 @@ urlpatterns = [ path("cat:/ce-mois-ci", month_view, name="ce_mois_ci_category"), path("mois///", month_view, name="month_view"), path("ce-mois-ci", month_view, name="ce_mois_ci"), + path("cat:/a-venir/", upcoming_events, name="a_venir_category"), + path( + "cat:/a-venir////", + upcoming_events, + name="a_venir_jour_category", + ), + path("a-venir/", upcoming_events, name="a_venir"), + path( + "a-venir////", + upcoming_events, + name="a_venir_jour", + ), path( "cat:/semaine///", week_view, @@ -236,11 +248,24 @@ urlpatterns = [ path("duplicates//fix", fix_duplicate, name="fix_duplicate"), # General pages path("a-propos", about, name="about"), + path("administration/", administration, name="administration"), path("activite/", activite, name="activite"), + path("cache/clear", clear_cache, name="clear_cache"), path("", home, name="home"), path("besoin-pour-import", import_requirements, name="import_requirements"), path("mentions-legales", mentions_legales, name="mentions_legales"), path("regles-de-moderation", moderation_rules, name="moderation_rules"), + path( + "static-content/create", + StaticContentCreateView.as_view(), + name="create_static_content", + ), + path( + "static-content//edit", + StaticContentUpdateView.as_view(), + name="edit_static_content", + ), + path("profile/edit", UserProfileUpdateView.as_view(), name="edit_profile"), path("statistiques", statistics, name="statistics"), path("merci", thank_you, name="thank_you"), # Import batch @@ -436,20 +461,7 @@ urlpatterns = [ path("tags/add", TagCreateView.as_view(), name="add_tag"), # TODO pas encore trié path("cat:/", home, name="home_category"), - path("cat:/a-venir/", upcoming_events, name="a_venir_category"), - path( - "cat:/a-venir////", - upcoming_events, - name="a_venir_jour_category", - ), - path("a-venir/", upcoming_events, name="a_venir"), - path( - "a-venir////", - upcoming_events, - name="a_venir_jour", - ), path("recent/", recent, name="recent"), - path("administration/", administration, name="administration"), path( "event////-", EventDetailView.as_view(), @@ -488,16 +500,6 @@ urlpatterns = [ path("admin/", admin.site.urls), path("accounts/", include("django.contrib.auth.urls")), path("test_app/", include("test_app.urls")), - path( - "static-content/create", - StaticContentCreateView.as_view(), - name="create_static_content", - ), - path( - "static-content//edit", - StaticContentUpdateView.as_view(), - name="edit_static_content", - ), path("rimports//stats", statistics, name="stats_rimport"), re_path(r"^robots\.txt", include("robots.urls")), path("__debug__/", include("debug_toolbar.urls")), @@ -508,8 +510,6 @@ urlpatterns = [ {"sitemaps": sitemaps}, name="cached-sitemap", ), - path("cache/clear", clear_cache, name="clear_cache"), - path("profile/edit", UserProfileUpdateView.as_view(), name="edit_profile"), ] if settings.DEBUG: diff --git a/src/agenda_culturel/views/calendar_views.py b/src/agenda_culturel/views/calendar_views.py index 3943a31..39bb60f 100644 --- a/src/agenda_culturel/views/calendar_views.py +++ b/src/agenda_culturel/views/calendar_views.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import date, timedelta from django.http import ( HttpResponseRedirect, @@ -7,9 +7,8 @@ from django.shortcuts import render from django.urls import reverse_lazy from django.utils.timezone import datetime -from . import upcoming_events from .utils import get_event_qs -from ..calendar import CalendarMonth, CalendarWeek +from ..calendar import CalendarMonth, CalendarWeek, CalendarList from ..filters import EventFilter from ..models import Category @@ -141,3 +140,49 @@ def week_view(request, year=None, week=None, home=False, cat=None): if home: context["home"] = 1 return render(request, "agenda_culturel/page-week.html", context) + + +def upcoming_events(request, year=None, month=None, day=None, neighsize=1, cat=None): + now = date.today() + if year is None: + year = now.year + if month is None: + month = now.month + if day is None: + day = now.day + + day = date(year, month, day) + day = day + timedelta(days=neighsize) + + request = EventFilter.set_default_values(request) + qs = get_event_qs(request).select_related("exact_location") + if cat is not None: + category = Category.objects.filter(slug=cat).first() + qs = qs.filter(category=category) + else: + category = None + + filter = EventFilter(request.GET, qs, request=request) + + if filter.has_category_parameters(): + return HttpResponseRedirect(filter.get_new_url()) + + cal = CalendarList( + day + timedelta(days=-neighsize), + day + timedelta(days=neighsize), + filter, + True, + ) + + context = { + "calendar": cal, + "now": now, + "day": day, + "init_date": now if cal.today_in_calendar() else day, + "filter": filter, + "date_pred": day + timedelta(days=-neighsize - 1), + "date_next": day + timedelta(days=neighsize + 1), + "category": category, + } + + return render(request, "agenda_culturel/page-upcoming.html", context) diff --git a/src/agenda_culturel/views/general_pages_views.py b/src/agenda_culturel/views/general_pages_views.py index 685954a..9998f0b 100644 --- a/src/agenda_culturel/views/general_pages_views.py +++ b/src/agenda_culturel/views/general_pages_views.py @@ -1,16 +1,30 @@ import calendar as _calendar from datetime import date, timedelta, datetime +from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.db.models import Count, F +from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.core.cache import cache +from django.db.models import Count, F, Subquery, OuterRef from django.db.models import Q, Min, Max, Avg, StdDev from django.db.models.functions import TruncMonth, ExtractDay +from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ +from django.views.generic import CreateView, UpdateView -from . import Median, week_view -from ..models import RecurrentImport, Event +from . import week_view +from .utils import Median +from ..forms import UserProfileForm +from ..models import ( + RecurrentImport, + Event, + StaticContent, + BatchImportation, + UserProfile, +) def home(request, cat=None): @@ -198,3 +212,137 @@ def activite(request): "weekly_modifications": weekly_modifications, }, ) + + +class StaticContentCreateView(LoginRequiredMixin, CreateView): + model = StaticContent + fields = ["text"] + permission_required = "agenda_culturel.add_staticcontent" + + def form_valid(self, form): + form.instance.name = self.request.GET["name"] + form.instance.url_path = self.request.GET["url_path"] + return super().form_valid(form) + + +class StaticContentUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = StaticContent + permission_required = "agenda_culturel.change_staticcontent" + fields = ["text"] + success_message = _("The static content has been successfully updated.") + + +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", + ) + + +@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() + + # get information about recent modifications + days = [today] + for i in range(0, 2): + days.append(days[-1] + timedelta(days=-1)) + daily_modifications = Event.get_count_modifications([(d, 1) for d in days]) + + # get last created events + events = ( + Event.objects.all() + .order_by("-created_date") + .select_related("exact_location", "category")[:5] + ) + + # get last batch imports + rel_event = Event.objects.filter( + import_sources__contains=[OuterRef("url_source")] + ).values("pk")[:1] + batch_imports = ( + BatchImportation.objects.all() + .select_related("recurrentImport") + .annotate(event_id=Subquery(rel_event)) + .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]) + ) + + nb_failed = imported_events.filter( + last_run_status=BatchImportation.STATUS.FAILED + ).count() + nb_canceled = imported_events.filter( + last_run_status=BatchImportation.STATUS.CANCELED + ).count() + nb_running = imported_events.filter( + last_run_status=BatchImportation.STATUS.RUNNING + ).count() + nb_all = imported_events.count() + + # get some info about imported (or not) events + srcs = RecurrentImport.objects.all().values_list("source") + in_future = Event.objects.filter(Q(start_day__gte=today)) + nb_in_rimport = in_future.filter(Q(import_sources__overlap=srcs)).count() + nb_in_orphan_import = in_future.filter( + ( + Q(import_sources__isnull=False) + & (Q(modified_date__isnull=True) | Q(modified_date__lte=F("imported_date"))) + ) + & ~Q(import_sources__overlap=srcs) + ).count() + + # get all non moderated events + nb_not_moderated = Event.get_nb_not_moderated(today, nb_mod_days, nb_classes) + + 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_in_rimport": nb_in_rimport, + "nb_in_orphan_import": nb_in_orphan_import, + }, + ) + + +class UserProfileUpdateView( + SuccessMessageMixin, + LoginRequiredMixin, + UpdateView, +): + model = UserProfile + success_message = _("Your user profile has been successfully modified.") + success_url = reverse_lazy("administration") + form_class = UserProfileForm + + def get_object(self): + return self.request.user.userprofile diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/oldviews.py index 321c5f9..c66a3d0 100644 --- a/src/agenda_culturel/views/oldviews.py +++ b/src/agenda_culturel/views/oldviews.py @@ -1,4 +1,3 @@ -import logging from datetime import date, timedelta from django.contrib import messages @@ -9,10 +8,7 @@ from django.contrib.auth.mixins import ( UserPassesTestMixin, ) from django.contrib.messages.views import SuccessMessageMixin -from django.core.cache import cache -from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.db.models import Aggregate, FloatField -from django.db.models import F, OuterRef, Q, Subquery +from django.core.paginator import EmptyPage, PageNotAnInteger from django.http import ( HttpResponseForbidden, HttpResponseRedirect, @@ -30,14 +26,12 @@ from django.views.generic.edit import ( UpdateView, ) -from .utils import get_event_qs -from ..calendar import CalendarList +from .utils import PaginatorFilter from ..celery import ( import_events_from_url, import_events_from_urls, ) from ..filters import ( - EventFilter, EventFilterAdmin, ) from ..forms import ( @@ -47,157 +41,14 @@ from ..forms import ( SimpleContactForm, URLSubmissionFormSet, URLSubmissionFormWithContact, - UserProfileForm, ) from ..import_tasks.extractor import Extractor from ..models import ( - BatchImportation, - Category, DuplicatedEvents, Event, Message, - RecurrentImport, - StaticContent, - UserProfile, ) -logger = logging.getLogger(__name__) - - -class Median(Aggregate): - function = "PERCENTILE_CONT" - name = "median" - output_field = FloatField() - template = "%(function)s(0.5) WITHIN GROUP (ORDER BY %(expressions)s)" - - -class PaginatorFilter(Paginator): - def __init__(self, filter, nb, request): - self.request = request - self.filter = filter - - super().__init__(filter.qs, nb) - - self.url_first_page = PaginatorFilter.update_param( - self.request.get_full_path(), "page", 1 - ) - self.url_last_page = PaginatorFilter.update_param( - self.request.get_full_path(), "page", self.num_pages - ) - - def update_param(params, key, value): - p = params.split("?") - root = p[0] - if len(p) > 1: - other = p[1] - others = other.split("&") - others = [o for o in others if not o.startswith(key)] - others += [key + "=" + str(value)] - return root + "?" + "&".join(others) - else: - return root + "?" + key + "=" + str(value) - - def page(self, *args, **kwargs): - page = super().page(*args, **kwargs) - - try: - page.url_previous_page = PaginatorFilter.update_param( - self.request.get_full_path(), - "page", - page.previous_page_number(), - ) - except EmptyPage: - page.url_previous_page = self.request.get_full_path() - - try: - page.url_next_page = PaginatorFilter.update_param( - self.request.get_full_path(), "page", page.next_page_number() - ) - except EmptyPage: - page.url_next_page = self.request.get_full_path() - - return page - - -# -# -# Useful for translation -to_be_translated = [ - _("Recurrent import name"), - _("Add another"), - _("Browse..."), - _("No file selected."), -] - - -def upcoming_events(request, year=None, month=None, day=None, neighsize=1, cat=None): - now = date.today() - if year is None: - year = now.year - if month is None: - month = now.month - if day is None: - day = now.day - - day = date(year, month, day) - day = day + timedelta(days=neighsize) - - request = EventFilter.set_default_values(request) - qs = get_event_qs(request).select_related("exact_location") - if cat is not None: - category = Category.objects.filter(slug=cat).first() - qs = qs.filter(category=category) - else: - category = None - - filter = EventFilter(request.GET, qs, request=request) - - if filter.has_category_parameters(): - return HttpResponseRedirect(filter.get_new_url()) - - cal = CalendarList( - day + timedelta(days=-neighsize), - day + timedelta(days=neighsize), - filter, - True, - ) - - context = { - "calendar": cal, - "now": now, - "day": day, - "init_date": now if cal.today_in_calendar() else day, - "filter": filter, - "date_pred": day + timedelta(days=-neighsize - 1), - "date_next": day + timedelta(days=neighsize + 1), - "category": category, - } - - return render(request, "agenda_culturel/page-upcoming.html", context) - - -class StaticContentCreateView(LoginRequiredMixin, CreateView): - model = StaticContent - fields = ["text"] - permission_required = "agenda_culturel.add_staticcontent" - - def form_valid(self, form): - form.instance.name = self.request.GET["name"] - form.instance.url_path = self.request.GET["url_path"] - return super().form_valid(form) - - -class StaticContentUpdateView( - SuccessMessageMixin, - PermissionRequiredMixin, - LoginRequiredMixin, - UpdateView, -): - model = StaticContent - permission_required = "agenda_culturel.change_staticcontent" - fields = ["text"] - success_message = _("The static content has been successfully updated.") - def update_from_source(request, pk): event = get_object_or_404(Event, pk=pk) @@ -691,91 +542,6 @@ def import_from_url(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() - - # get information about recent modifications - days = [today] - for i in range(0, 2): - days.append(days[-1] + timedelta(days=-1)) - daily_modifications = Event.get_count_modifications([(d, 1) for d in days]) - - # get last created events - events = ( - Event.objects.all() - .order_by("-created_date") - .select_related("exact_location", "category")[:5] - ) - - # get last batch imports - rel_event = Event.objects.filter( - import_sources__contains=[OuterRef("url_source")] - ).values("pk")[:1] - batch_imports = ( - BatchImportation.objects.all() - .select_related("recurrentImport") - .annotate(event_id=Subquery(rel_event)) - .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]) - ) - - nb_failed = imported_events.filter( - last_run_status=BatchImportation.STATUS.FAILED - ).count() - nb_canceled = imported_events.filter( - last_run_status=BatchImportation.STATUS.CANCELED - ).count() - nb_running = imported_events.filter( - last_run_status=BatchImportation.STATUS.RUNNING - ).count() - nb_all = imported_events.count() - - # get some info about imported (or not) events - srcs = RecurrentImport.objects.all().values_list("source") - in_future = Event.objects.filter(Q(start_day__gte=today)) - nb_in_rimport = in_future.filter(Q(import_sources__overlap=srcs)).count() - nb_in_orphan_import = in_future.filter( - ( - Q(import_sources__isnull=False) - & (Q(modified_date__isnull=True) | Q(modified_date__lte=F("imported_date"))) - ) - & ~Q(import_sources__overlap=srcs) - ).count() - - # get all non moderated events - nb_not_moderated = Event.get_nb_not_moderated(today, nb_mod_days, nb_classes) - - 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_in_rimport": nb_in_rimport, - "nb_in_orphan_import": nb_in_orphan_import, - }, - ) - - @login_required(login_url="/accounts/login/") @permission_required("agenda_culturel.view_event") def recent(request): @@ -797,29 +563,3 @@ def recent(request): "agenda_culturel/recent.html", {"filter": filter, "paginator_filter": response}, ) - - -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", - ) - - -class UserProfileUpdateView( - SuccessMessageMixin, - LoginRequiredMixin, - UpdateView, -): - model = UserProfile - success_message = _("Your user profile has been successfully modified.") - success_url = reverse_lazy("administration") - form_class = UserProfileForm - - def get_object(self): - return self.request.user.userprofile diff --git a/src/agenda_culturel/views/utils.py b/src/agenda_culturel/views/utils.py index a9f8b39..90a2210 100644 --- a/src/agenda_culturel/views/utils.py +++ b/src/agenda_culturel/views/utils.py @@ -1,6 +1,9 @@ -from ..models import Event +from django.core.paginator import EmptyPage, Paginator from django.utils.translation import gettext_lazy as _ +from django.db.models import Aggregate, FloatField +from ..models import Event + # # # Useful for translation @@ -17,3 +20,58 @@ def get_event_qs(request): return Event.objects.filter() else: return Event.objects.filter(status=Event.STATUS.PUBLISHED) + + +class Median(Aggregate): + function = "PERCENTILE_CONT" + name = "median" + output_field = FloatField() + template = "%(function)s(0.5) WITHIN GROUP (ORDER BY %(expressions)s)" + + +class PaginatorFilter(Paginator): + def __init__(self, filter, nb, request): + self.request = request + self.filter = filter + + super().__init__(filter.qs, nb) + + self.url_first_page = PaginatorFilter.update_param( + self.request.get_full_path(), "page", 1 + ) + self.url_last_page = PaginatorFilter.update_param( + self.request.get_full_path(), "page", self.num_pages + ) + + def update_param(params, key, value): + p = params.split("?") + root = p[0] + if len(p) > 1: + other = p[1] + others = other.split("&") + others = [o for o in others if not o.startswith(key)] + others += [key + "=" + str(value)] + return root + "?" + "&".join(others) + else: + return root + "?" + key + "=" + str(value) + + def page(self, *args, **kwargs): + page = super().page(*args, **kwargs) + + try: + page.url_previous_page = PaginatorFilter.update_param( + self.request.get_full_path(), + "page", + page.previous_page_number(), + ) + except EmptyPage: + page.url_previous_page = self.request.get_full_path() + + try: + page.url_next_page = PaginatorFilter.update_param( + self.request.get_full_path(), "page", page.next_page_number() + ) + except EmptyPage: + page.url_next_page = self.request.get_full_path() + + return page From dbc562231d7b5312caa1b9aa7dab153f47d60fe9 Mon Sep 17 00:00:00 2001 From: SebF Date: Tue, 15 Apr 2025 10:44:24 +0200 Subject: [PATCH 19/20] =?UTF-8?q?S=C3=A9paration=20des=20vues=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agenda_culturel/urls.py | 99 ++++++++++--------- src/agenda_culturel/views/__init__.py | 3 +- .../views/{oldviews.py => event_views.py} | 0 3 files changed, 51 insertions(+), 51 deletions(-) rename src/agenda_culturel/views/{oldviews.py => event_views.py} (100%) diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 0e26102..c2b3cbd 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -31,6 +31,17 @@ from .views import ( # Errors internal_server_error, page_not_found, + # Events + recent, + EventDetailView, + EventUpdateView, + EventCreateView, + update_from_source, + change_status_event, + EventDeleteView, + import_event_proxy, + import_from_url, + import_from_urls, # Event duplicates DuplicatedEventsDetailView, duplicates, @@ -110,17 +121,6 @@ from .views import ( rename_tag, delete_tag, TagCreateView, - # TODO pas encore trié - recent, - EventDetailView, - EventUpdateView, - EventCreateView, - update_from_source, - change_status_event, - EventDeleteView, - import_event_proxy, - import_from_url, - import_from_urls, ) event_dict = { @@ -192,6 +192,7 @@ urlpatterns = [ path("semaine///", week_view, name="week_view"), path("cette-semaine/", week_view, name="cette_semaine"), # Categorisation rules + path("cat:/", home, name="home_category"), path( "catrules/add", CategorisationRuleCreateView.as_view(), @@ -223,6 +224,43 @@ urlpatterns = [ update_duplicate_event, name="update_event", ), + # Events + path("recent/", recent, name="recent"), + path( + "event////-", + EventDetailView.as_view(), + name="view_event", + ), + path("event//", EventDetailView.as_view(), name="edit_event_pk"), + path("event//edit", EventUpdateView.as_view(), name="edit_event"), + path( + "event//edit-force", EventUpdateView.as_view(), name="edit_event_force" + ), + path( + "event//simple-clone/edit", + EventUpdateView.as_view(), + name="simple_clone_edit", + ), + path( + "event//clone/edit", + EventUpdateView.as_view(), + name="clone_edit", + ), + path( + "event//update-from-source", + update_from_source, + name="update_from_source", + ), + path( + "event//change-status/", + change_status_event, + name="change_status_event", + ), + path("event//delete", EventDeleteView.as_view(), name="delete_event"), + path("ajouter", import_event_proxy, name="add_event"), + path("ajouter/url", import_from_url, name="add_event_url"), + path("ajouter/urls", import_from_urls, name="add_event_urls"), + path("ajouter/details", EventCreateView.as_view(), name="add_event_details"), # Event duplicates path( "duplicates/", @@ -459,44 +497,7 @@ urlpatterns = [ path("tag//rename", rename_tag, name="rename_tag"), path("tag//delete", delete_tag, name="delete_tag"), path("tags/add", TagCreateView.as_view(), name="add_tag"), - # TODO pas encore trié - path("cat:/", home, name="home_category"), - path("recent/", recent, name="recent"), - path( - "event////-", - EventDetailView.as_view(), - name="view_event", - ), - path("event//", EventDetailView.as_view(), name="edit_event_pk"), - path("event//edit", EventUpdateView.as_view(), name="edit_event"), - path( - "event//edit-force", EventUpdateView.as_view(), name="edit_event_force" - ), - path( - "event//simple-clone/edit", - EventUpdateView.as_view(), - name="simple_clone_edit", - ), - path( - "event//clone/edit", - EventUpdateView.as_view(), - name="clone_edit", - ), - path( - "event//update-from-source", - update_from_source, - name="update_from_source", - ), - path( - "event//change-status/", - change_status_event, - name="change_status_event", - ), - path("event//delete", EventDeleteView.as_view(), name="delete_event"), - path("ajouter", import_event_proxy, name="add_event"), - path("ajouter/url", import_from_url, name="add_event_url"), - path("ajouter/urls", import_from_urls, name="add_event_urls"), - path("ajouter/details", EventCreateView.as_view(), name="add_event_details"), + # Django path("admin/", admin.site.urls), path("accounts/", include("django.contrib.auth.urls")), path("test_app/", include("test_app.urls")), diff --git a/src/agenda_culturel/views/__init__.py b/src/agenda_culturel/views/__init__.py index 1fbeca5..5394856 100644 --- a/src/agenda_culturel/views/__init__.py +++ b/src/agenda_culturel/views/__init__.py @@ -1,7 +1,6 @@ -from .oldviews import * - from .calendar_views import * from .categorisation_rules_view import * +from .event_views import * from .event_duplicate_views import * from .errors import * from .export_views import * diff --git a/src/agenda_culturel/views/oldviews.py b/src/agenda_culturel/views/event_views.py similarity index 100% rename from src/agenda_culturel/views/oldviews.py rename to src/agenda_culturel/views/event_views.py From 4f673a8990afae3ab8dafaeff63eb44b6e9352b3 Mon Sep 17 00:00:00 2001 From: Jean-Marie Favreau Date: Wed, 16 Apr 2025 13:39:22 +0200 Subject: [PATCH 20/20] Ajout fichier manquant --- .../views/special_period_views.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/agenda_culturel/views/special_period_views.py diff --git a/src/agenda_culturel/views/special_period_views.py b/src/agenda_culturel/views/special_period_views.py new file mode 100644 index 0000000..f246513 --- /dev/null +++ b/src/agenda_culturel/views/special_period_views.py @@ -0,0 +1,106 @@ +from django.contrib import messages +from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin +from django.contrib.messages.views import SuccessMessageMixin +from django.http import HttpResponseRedirect +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _, ngettext +from django.views.generic import ListView, UpdateView, CreateView, DeleteView +from django.shortcuts import render + +from ..forms import SpecialPeriodFileForm, SpecialPeriodForm +from ..models import SpecialPeriod + + +class SpecialPeriodCreateView( + PermissionRequiredMixin, LoginRequiredMixin, SuccessMessageMixin, CreateView +): + model = SpecialPeriod + permission_required = "agenda_culturel.add_specialperiod" + success_message = _("The special period has been successfully created.") + success_url = reverse_lazy("list_specialperiods") + form_class = SpecialPeriodForm + + +class SpecialPeriodListView(PermissionRequiredMixin, LoginRequiredMixin, ListView): + model = SpecialPeriod + paginate_by = 10 + permission_required = "agenda_culturel.add_specialperiod" + ordering = ["start_date", "name__unaccent"] + + +class SpecialPeriodDeleteView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + DeleteView, +): + model = SpecialPeriod + permission_required = "agenda_culturel.delete_specialperiod" + success_url = reverse_lazy("list_specialperiods") + success_message = _("The special period has been successfully deleted.") + + +class SpecialPeriodUpdateView( + SuccessMessageMixin, + PermissionRequiredMixin, + LoginRequiredMixin, + UpdateView, +): + model = SpecialPeriod + permission_required = "agenda_culturel.change_specialperiod" + success_message = _("The special period has been successfully updated.") + success_url = reverse_lazy("list_specialperiods") + form_class = SpecialPeriodForm + + +def load_specialperiods_from_ical(request): + if request.method == "POST": + form = SpecialPeriodFileForm(request.POST, request.FILES) + if form.is_valid(): + nb_created, nb_overlap, nb_error, error = SpecialPeriod.load_from_ical( + request.FILES["file"], request.POST["periodtype"] + ) + + if nb_created > 0: + messages.success( + request, + ngettext( + "%(nb_created)d interval inserted.", + "%(nb_created)d intervals inserted.", + nb_created, + ) + % {"nb_created": nb_created}, + ) + + if nb_overlap > 0: + messages.success( + request, + ngettext( + "%(nb_overlap)d insersion was not possible due to overlap.", + "%(nb_overlap)d insersion were not possible due to overlap.", + nb_overlap, + ) + % {"nb_overlap": nb_overlap}, + ) + + if nb_error > 0: + messages.success( + request, + ngettext( + "%(nb_error)d error while reading ical file.", + "%(nb_error)d error while reading ical file.", + nb_error, + ) + % {"nb_error": nb_error}, + ) + if error is not None: + messages.success( + request, _("Error during file reading: {}").format(error) + ) + + return HttpResponseRedirect(reverse_lazy("list_specialperiods")) + else: + form = SpecialPeriodFileForm() + return render( + request, "agenda_culturel/load_specialperiods_from_ical.html", {"form": form} + )