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:
parent
794bed6b74
commit
41d6448077
@ -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)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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:
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
@ -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."
|
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."
|
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é."
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
|
||||||
|
@ -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,14 +386,20 @@ $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;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorlist {
|
.errorlist {
|
||||||
@extend .message.danger;
|
@extend .message, .danger;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding-left: 3.7em;
|
padding-left: 3.7em;
|
||||||
}
|
}
|
||||||
|
@ -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')),
|
||||||
|
@ -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:
|
||||||
|
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:
|
else:
|
||||||
messages.success(self.request, _("The URL has been submitted and the associated event will be integrated in the agenda after validation."))
|
return render(request, 'agenda_culturel/event_form.html', context={'form': form })
|
||||||
create_event_from_submission.delay(url)
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = EventSubmissionForm()
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
if request.method == 'POST':
|
||||||
if self.request.user.is_authenticated:
|
form = EventSubmissionForm(request.POST)
|
||||||
return reverse_lazy("view_all_events")
|
|
||||||
else:
|
|
||||||
return reverse_lazy("home")
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user