L'import des événements ne se fait plus côté Celery, mais directement en django, ce qui permet :

- aux internautes d'éditer le résultat de l'import avant de le soumettre. Fix #45
- de vérifier que l'url n'existe pas déjà. Fix #31
-
This commit is contained in:
Jean-Marie Favreau 2023-11-26 16:00:02 +01:00
parent 794bed6b74
commit 41d6448077
10 changed files with 134 additions and 89 deletions

View File

@ -1,12 +1,11 @@
from django.contrib import admin from django.contrib import admin
from django import forms from django import forms
from .models import Event, EventSubmissionForm, Category, StaticContent from .models import Event, Category, StaticContent
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
from django_better_admin_arrayfield.models.fields import DynamicArrayField from django_better_admin_arrayfield.models.fields import DynamicArrayField
admin.site.register(EventSubmissionForm)
admin.site.register(Category) admin.site.register(Category)
admin.site.register(StaticContent) admin.site.register(StaticContent)

View File

@ -26,25 +26,6 @@ app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks() app.autodiscover_tasks()
@app.task(bind=True)
def create_event_from_submission(self, url):
from agenda_culturel.models import Event
logger.info(f"{url=}")
if len(Event.objects.filter(reference_urls__contains=[url])) != 0:
logger.info("Already known url: %s", url)
else:
try:
logger.info("About to create event from submission")
events = ExtractorAllURLs.extract(url)
if events != None:
for e in events:
e.save()
except Exception as e:
logger.error(e)
app.conf.timezone = "Europe/Paris" app.conf.timezone = "Europe/Paris"

View File

@ -15,13 +15,10 @@ import os
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import json import json
from datetime import datetime from datetime import datetime, date
import logging
logger = logging.getLogger(__name__)
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
class Extractor: class Extractor:
@ -128,9 +125,13 @@ class ExtractorFacebook(Extractor):
return self.elements[key] if key in self.elements else None return self.elements[key] if key in self.elements else None
def get_element_datetime(self, key): def get_element_date(self, key):
v = self.get_element(key) v = self.get_element(key)
return datetime.fromtimestamp(v) if v is not None else None return datetime.fromtimestamp(v).date() if v is not None and v != 0 else None
def get_element_time(self, key):
v = self.get_element(key)
return datetime.fromtimestamp(v).strftime('%H:%M') if v is not None and v != 0 else None
def add_fragment(self, i, event): def add_fragment(self, i, event):
self.fragments[i] = event self.fragments[i] = event
@ -171,7 +172,7 @@ class ExtractorFacebook(Extractor):
if "end_timestamp" not in self.elements and len(self.possible_end_timestamp) != 0: if "end_timestamp" not in self.elements and len(self.possible_end_timestamp) != 0:
for s in self.possible_end_timestamp: for s in self.possible_end_timestamp:
if s["start_timestamp"] == self.elements["start_timestamp"]: if "start_timestamp" in s and "start_timestamp" in self.elements and s["start_timestamp"] == self.elements["start_timestamp"]:
self.elements["end_timestamp"] = s["end_timestamp"] self.elements["end_timestamp"] = s["end_timestamp"]
break break
@ -207,13 +208,12 @@ class ExtractorFacebook(Extractor):
local_image = None if image is None else Extractor.download_media(image) local_image = None if image is None else Extractor.download_media(image)
return Event(title=self.get_element("name"), return Event(title=self.get_element("name"),
status=Event.STATUS.DRAFT, status=Event.STATUS.DRAFT,
start_day=self.get_element_datetime("start_timestamp"), start_day=self.get_element_date("start_timestamp"),
start_time=self.get_element_datetime("start_timestamp"), start_time=self.get_element_time("start_timestamp"),
end_day=self.get_element_datetime("end_timestamp"), end_day=self.get_element_date("end_timestamp"),
end_time=self.get_element_datetime("end_timestamp"), end_time=self.get_element_time("end_timestamp"),
location=self.get_element("event_place_name"), location=self.get_element("event_place_name"),
description=self.get_element("description"), description=self.get_element("description"),
local_image=local_image, local_image=local_image,
@ -226,7 +226,7 @@ class ExtractorFacebook(Extractor):
if ExtractorFacebook.is_known_url(url): if ExtractorFacebook.is_known_url(url):
u = urlparse(url) u = urlparse(url)
return u.scheme + "://" + u.netloc + u.path return "https://www.facebook.com" + u.path
else: else:
return url return url
@ -247,7 +247,7 @@ class ExtractorFacebook(Extractor):
if fevent is not None: if fevent is not None:
logger.info("Facebook event: " + str(fevent)) logger.info("Facebook event: " + str(fevent))
result = fevent.build_event(url) result = fevent.build_event(url)
return [result] return result
return None return None
@ -272,7 +272,6 @@ class ExtractorAllURLs:
for e in ExtractorAllURLs.extractors: for e in ExtractorAllURLs.extractors:
result = e.process_page(txt, url) result = e.process_page(txt, url)
if result is not None: if result is not None:
return result return result
else: else:

View File

@ -1,15 +1,12 @@
from django.forms import ModelForm, ValidationError, TextInput from django.forms import ModelForm, ValidationError, TextInput, Form, URLField
from django.views.generic import FormView
from datetime import date from datetime import date
from .models import EventSubmissionForm, Event from .models import Event
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class EventSubmissionModelForm(ModelForm): class EventSubmissionForm(Form):
class Meta: url = URLField(max_length=512)
model = EventSubmissionForm
fields = ["url"]
class EventForm(ModelForm): class EventForm(ModelForm):
@ -25,9 +22,9 @@ class EventForm(ModelForm):
} }
def __init__(self, instance, *args, **kwargs): def __init__(self, *args, **kwargs):
is_authenticated = kwargs.pop('is_authenticated', False) is_authenticated = kwargs.pop('is_authenticated', False)
super().__init__(instance=instance, *args, **kwargs) super().__init__(*args, **kwargs)
if not is_authenticated: if not is_authenticated:
del self.fields['status'] del self.fields['status']

View File

@ -254,3 +254,21 @@ msgstr "L'événement a été soumis et sera publié dès qu'il aura été valid
msgid "The URL has been taken into account, and the associated event will be available in a few moments for validation." msgid "The URL has been taken into account, and the associated event will be available in a few moments for validation."
msgstr "L'URL a été prise en compte, et l'événement associé sera disponible dans quelques instants pour validation." msgstr "L'URL a été prise en compte, et l'événement associé sera disponible dans quelques instants pour validation."
msgid "The event has been successfully extracted, and you can now submit it after modifying it if necessary."
msgstr "L'événement a été extrait avec succès, vous pouvez maintenant le soumettre après l'avoir modifié au besoin."
msgid "Unable to extract an event from the proposed URL. Please use the form below to submit the event."
msgstr "Impossible d'extraire un événement depuis l'URL proposée. Veuillez utiliser le formulaire ci-dessous pour soumettre l'événement."
msgid "This URL has already been submitted, and you can find the event below."
msgstr "Cette URL a déjà été soumise, et vous trouverez l'événement ci-dessous."
msgid "This URL has already been submitted and is awaiting moderation."
msgstr "Cette URL a déjà été soumise, et est en attente de modération"
msgid "This URL has already been submitted, but has not been selected for publication by the moderation team."
msgstr "Cette URL a déjà été soumise, mais n'a pas été retenue par l'équipe de modération pour la publication."
msgid "The event is saved."
msgstr "L'événement est enregistré."

View File

@ -0,0 +1,16 @@
# Generated by Django 4.2.7 on 2023-11-26 12:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0004_alter_event_category'),
]
operations = [
migrations.DeleteModel(
name='EventSubmissionForm',
),
]

View File

@ -172,15 +172,3 @@ class Event(models.Model):
def modified(self): def modified(self):
return abs((self.modified_date - self.created_date).total_seconds()) > 1 return abs((self.modified_date - self.created_date).total_seconds()) > 1
class EventSubmissionForm(models.Model):
url = models.URLField(max_length=512, verbose_name=_('URL'), help_text=_("URL where this event can be found."))
class Meta:
db_table = "eventsubmissionform"
verbose_name = _("Event submission form")
verbose_name_plural = _("Event submissions forms")
def __str__(self):
return self.url

View File

@ -348,7 +348,8 @@ $green-50: #e8f5e9 !default;
$green-800: #1b5e20 !default; $green-800: #1b5e20 !default;
$red-50: #ffebee !default; $red-50: #ffebee !default;
$red-900: #b71c1c !default; $red-900: #b71c1c !default;
$yellow-50: #ecf4a4 !default;
$yellow-900: #616918 !default;
// simple picocss alerts // simple picocss alerts
// inherit responsive typography, responsive spacing, icons and size // inherit responsive typography, responsive spacing, icons and size
@ -385,6 +386,12 @@ $red-900: #b71c1c !default;
--icon: var(--icon-valid); --icon: var(--icon-valid);
--color: #{$green-800}; --color: #{$green-800};
} }
.message.info {
--background-color: #{$yellow-50};
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{rgba(darken($yellow-900, 15%), .999)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
--color: #{$yellow-900};
}
.footer { .footer {
opacity: 0.7; opacity: 0.7;
@ -392,7 +399,7 @@ $red-900: #b71c1c !default;
} }
.errorlist { .errorlist {
@extend .message.danger; @extend .message, .danger;
margin-left: 0; margin-left: 0;
padding-left: 3.7em; padding-left: 3.7em;
} }

View File

@ -21,7 +21,7 @@ urlpatterns = [
path("event/<int:pk>-<extra>", EventDetailView.as_view(), name="view_event"), path("event/<int:pk>-<extra>", EventDetailView.as_view(), name="view_event"),
path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"), path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
path("event/<int:pk>/delete", EventDeleteView.as_view(), name="delete_event"), path("event/<int:pk>/delete", EventDeleteView.as_view(), name="delete_event"),
path("importer", EventSubmissionFormView.as_view(), name="event_import_form"), path("importer", import_from_url, name="event_import_form"),
path("ajouter", EventCreateView.as_view(), name="add_event"), path("ajouter", EventCreateView.as_view(), name="add_event"),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),

View File

@ -7,9 +7,11 @@ from django import forms
from django.contrib.postgres.search import SearchQuery, SearchHeadline from django.contrib.postgres.search import SearchQuery, SearchHeadline
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.urls import reverse
import urllib
from .forms import EventSubmissionModelForm, EventForm from .forms import EventSubmissionForm, EventForm
from .celery import create_event_from_submission
from .models import Event, Category, StaticContent from .models import Event, Category, StaticContent
from django.utils import timezone from django.utils import timezone
@ -28,6 +30,7 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from .calendar import CalendarMonth, CalendarWeek from .calendar import CalendarMonth, CalendarWeek
from .extractors import ExtractorAllURLs
import unicodedata import unicodedata
@ -213,6 +216,11 @@ class EventUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
form_class = EventForm form_class = EventForm
success_message = _('The event has been successfully modified.') success_message = _('The event has been successfully modified.')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['is_authenticated'] = self.request.user.is_authenticated
return kwargs
class EventDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView): class EventDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
model = Event model = Event
@ -224,40 +232,72 @@ class EventDetailView(UserPassesTestMixin, DetailView):
model = Event model = Event
template_name = "agenda_culturel/page-event.html" template_name = "agenda_culturel/page-event.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["now"] = timezone.now()
return context
def test_func(self): def test_func(self):
return self.request.user.is_authenticated or self.get_object().status != Event.STATUS.PUBLISHED return self.request.user.is_authenticated or self.get_object().status != Event.STATUS.PUBLISHED
class EventSubmissionFormView(FormView): def import_from_url(request):
form_class = EventSubmissionModelForm
template_name = "agenda_culturel/import.html"
def form_valid(self, form): import logging
form.save() logger = logging.getLogger(__name__)
self.create_event(form.cleaned_data)
return super().form_valid(form) if "title" in request.POST and request.method == 'POST':
logger.error("on passe l")
def create_event(self, valid_data): form = EventForm(request.POST)
url = valid_data["url"] logger.error("on passe i")
if self.request.user.is_authenticated: if form.is_valid():
messages.success(self.request, _("The URL has been taken into account, and the associated event will be available in a few moments for validation.")) logger.error("valide")
new_event = form.save()
if request.user.is_authenticated:
messages.success(request, _("The event is saved."))
return HttpResponseRedirect(new_event.get_absolute_url())
else: else:
messages.success(self.request, _("The URL has been submitted and the associated event will be integrated in the agenda after validation.")) messages.success(request, _("The event has been submitted and will be published as soon as it has been validated by the moderation team."))
create_event_from_submission.delay(url) return HttpResponseRedirect(reverse("home"))
def get_success_url(self, **kwargs):
if self.request.user.is_authenticated:
return reverse_lazy("view_all_events")
else: else:
return reverse_lazy("home") return render(request, 'agenda_culturel/event_form.html', context={'form': form })
else:
form = EventSubmissionForm()
if request.method == 'POST':
form = EventSubmissionForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
url = cd.get('url')
url = ExtractorAllURLs.clean_url(url)
existing = Event.objects.filter(reference_urls__contains=[url])
if len(existing) == 0:
event = ExtractorAllURLs.extract(url)
if event != None:
form = EventForm(instance=event)
messages.success(request, _("The event has been successfully extracted, and you can now submit it after modifying it if necessary."))
return render(request, 'agenda_culturel/event_form.html', context={'form': form })
else:
form = EventForm(initial={'url': [url]})
messages.error(request, _("Unable to extract an event from the proposed URL. Please use the form below to submit the event."))
return render(request, 'agenda_culturel/importer.html', context={'form': form })
else:
published = [e for e in existing if e.status == Event.STATUS.PUBLISHED]
drafts = [e for e in existing if e.status == Event.STATUS.DRAFT]
trash = [e for e in existing if e.status == Event.STATUS.TRASH]
if request.user.is_authenticated or len(published) > 1:
event = published[0] if len(published) > 1 else existing[0]
messages.info(request, _("This URL has already been submitted, and you can find the event below."))
return HttpResponseRedirect(event.get_absolute_url())
else:
if len(drafts) > 0:
messages.info(request, _("This URL has already been submitted, but has not been selected for publication by the moderation team."))
elif len(trash) > 0:
messages.info(request, _("This URL has already been submitted and is awaiting moderation."))
return render(request, 'agenda_culturel/import.html', context={'form': form })
class EventFilterAdmin(django_filters.FilterSet): class EventFilterAdmin(django_filters.FilterSet):
status = django_filters.MultipleChoiceFilter(choices=Event.STATUS.choices, widget=forms.CheckboxSelectMultiple) status = django_filters.MultipleChoiceFilter(choices=Event.STATUS.choices, widget=forms.CheckboxSelectMultiple)