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 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.forms.widgets import DynamicArrayWidget
from django_better_admin_arrayfield.models.fields import DynamicArrayField
admin.site.register(EventSubmissionForm)
admin.site.register(Category)
admin.site.register(StaticContent)

View File

@ -26,25 +26,6 @@ app.config_from_object("django.conf:settings", namespace="CELERY")
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"

View File

@ -15,13 +15,10 @@ import os
from bs4 import BeautifulSoup
import json
from datetime import datetime
from datetime import datetime, date
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
import logging
logger = logging.getLogger(__name__)
class Extractor:
@ -128,9 +125,13 @@ class ExtractorFacebook(Extractor):
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)
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):
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:
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"]
break
@ -207,13 +208,12 @@ class ExtractorFacebook(Extractor):
local_image = None if image is None else Extractor.download_media(image)
return Event(title=self.get_element("name"),
status=Event.STATUS.DRAFT,
start_day=self.get_element_datetime("start_timestamp"),
start_time=self.get_element_datetime("start_timestamp"),
end_day=self.get_element_datetime("end_timestamp"),
end_time=self.get_element_datetime("end_timestamp"),
start_day=self.get_element_date("start_timestamp"),
start_time=self.get_element_time("start_timestamp"),
end_day=self.get_element_date("end_timestamp"),
end_time=self.get_element_time("end_timestamp"),
location=self.get_element("event_place_name"),
description=self.get_element("description"),
local_image=local_image,
@ -226,7 +226,7 @@ class ExtractorFacebook(Extractor):
if ExtractorFacebook.is_known_url(url):
u = urlparse(url)
return u.scheme + "://" + u.netloc + u.path
return "https://www.facebook.com" + u.path
else:
return url
@ -247,7 +247,7 @@ class ExtractorFacebook(Extractor):
if fevent is not None:
logger.info("Facebook event: " + str(fevent))
result = fevent.build_event(url)
return [result]
return result
return None
@ -272,7 +272,6 @@ class ExtractorAllURLs:
for e in ExtractorAllURLs.extractors:
result = e.process_page(txt, url)
if result is not None:
return result
else:

View File

@ -1,15 +1,12 @@
from django.forms import ModelForm, ValidationError, TextInput
from django.views.generic import FormView
from django.forms import ModelForm, ValidationError, TextInput, Form, URLField
from datetime import date
from .models import EventSubmissionForm, Event
from .models import Event
from django.utils.translation import gettext_lazy as _
class EventSubmissionModelForm(ModelForm):
class Meta:
model = EventSubmissionForm
fields = ["url"]
class EventSubmissionForm(Form):
url = URLField(max_length=512)
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)
super().__init__(instance=instance, *args, **kwargs)
super().__init__(*args, **kwargs)
if not is_authenticated:
del self.fields['status']

View File

@ -253,4 +253,22 @@ msgid "The event has been submitted and will be published as soon as it has been
msgstr "L'événement a été soumis et sera publié dès qu'il aura été validé par l'équipe de modération."
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):
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;
$red-50: #ffebee !default;
$red-900: #b71c1c !default;
$yellow-50: #ecf4a4 !default;
$yellow-900: #616918 !default;
// simple picocss alerts
// inherit responsive typography, responsive spacing, icons and size
@ -385,14 +386,20 @@ $red-900: #b71c1c !default;
--icon: var(--icon-valid);
--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 {
opacity: 0.7;
font-size: 80%;
}
.errorlist {
@extend .message.danger;
@extend .message, .danger;
margin-left: 0;
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>/edit", EventUpdateView.as_view(), name="edit_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("admin/", admin.site.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.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.urls import reverse
import urllib
from .forms import EventSubmissionModelForm, EventForm
from .celery import create_event_from_submission
from .forms import EventSubmissionForm, EventForm
from .models import Event, Category, StaticContent
from django.utils import timezone
@ -28,6 +30,7 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from .calendar import CalendarMonth, CalendarWeek
from .extractors import ExtractorAllURLs
import unicodedata
@ -213,6 +216,11 @@ class EventUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
form_class = EventForm
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):
model = Event
@ -224,40 +232,72 @@ class EventDetailView(UserPassesTestMixin, DetailView):
model = Event
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):
return self.request.user.is_authenticated or self.get_object().status != Event.STATUS.PUBLISHED
class EventSubmissionFormView(FormView):
form_class = EventSubmissionModelForm
template_name = "agenda_culturel/import.html"
def import_from_url(request):
def form_valid(self, form):
form.save()
self.create_event(form.cleaned_data)
import logging
logger = logging.getLogger(__name__)
return super().form_valid(form)
def create_event(self, valid_data):
url = valid_data["url"]
if self.request.user.is_authenticated:
messages.success(self.request, _("The URL has been taken into account, and the associated event will be available in a few moments for validation."))
if "title" in request.POST and request.method == 'POST':
logger.error("on passe l")
form = EventForm(request.POST)
logger.error("on passe i")
if form.is_valid():
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:
messages.success(request, _("The event has been submitted and will be published as soon as it has been validated by the moderation team."))
return HttpResponseRedirect(reverse("home"))
else:
messages.success(self.request, _("The URL has been submitted and the associated event will be integrated in the agenda after validation."))
create_event_from_submission.delay(url)
return render(request, 'agenda_culturel/event_form.html', context={'form': form })
else:
form = EventSubmissionForm()
def get_success_url(self, **kwargs):
if self.request.user.is_authenticated:
return reverse_lazy("view_all_events")
else:
return reverse_lazy("home")
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):
status = django_filters.MultipleChoiceFilter(choices=Event.STATUS.choices, widget=forms.CheckboxSelectMultiple)