Ajout d'un mécanisme d'import par batches (pour l'instant pas connecté au celery)

This commit is contained in:
Jean-Marie Favreau 2023-12-23 09:43:34 +01:00
parent 6ac3afc0e5
commit e8712bd64c
10 changed files with 352 additions and 83 deletions

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: agenda_culturel\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-16 20:13+0000\n"
"POT-Creation-Date: 2023-12-23 08:38+0000\n"
"PO-Revision-Date: 2023-10-29 14:16+0000\n"
"Last-Translator: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
"Language-Team: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
@ -17,262 +17,290 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: agenda_culturel/forms.py:36
msgid "The start date cannot be earler than today."
msgstr "La date de début ne peut pas être avant aujourd'hui."
#: agenda_culturel/forms.py:46
#: agenda_culturel/forms.py:37
msgid "The end date must be after the start date."
msgstr "La date de fin doit être après la date de début."
#: agenda_culturel/forms.py:48
msgid "The end date cannot be earler than today."
msgstr "La date de fin ne peut pas être avant aujourd'hui."
#: agenda_culturel/forms.py:63
#: agenda_culturel/forms.py:52
msgid "The end time cannot be earlier than the start time."
msgstr "L'heure de fin ne peut pas être avant l'heure de début."
#: agenda_culturel/models.py:19 agenda_culturel/models.py:48
#: agenda_culturel/models.py:183
#: agenda_culturel/models.py:23 agenda_culturel/models.py:52
#: agenda_culturel/models.py:211
msgid "Name"
msgstr "Nom"
#: agenda_culturel/models.py:19 agenda_culturel/models.py:48
#: agenda_culturel/models.py:23 agenda_culturel/models.py:52
msgid "Category name"
msgstr "Nom de la catégorie"
#: agenda_culturel/models.py:20
#: agenda_culturel/models.py:24
msgid "Content"
msgstr "Contenu"
#: agenda_culturel/models.py:20
#: agenda_culturel/models.py:24
msgid "Text as shown to the visitors"
msgstr "Text tel que présenté aux visiteureuses"
#: agenda_culturel/models.py:21
#: agenda_culturel/models.py:25
msgid "URL path"
msgstr ""
#: agenda_culturel/models.py:21
#: agenda_culturel/models.py:25
msgid "URL path where the content is included."
msgstr ""
#: agenda_culturel/models.py:49
#: agenda_culturel/models.py:53
msgid "Alternative Name"
msgstr "Nom alternatif"
#: agenda_culturel/models.py:49
#: agenda_culturel/models.py:53
msgid "Alternative name used with a time period"
msgstr "Nom alternatif utilisé avec une période de temps"
#: agenda_culturel/models.py:50
#: agenda_culturel/models.py:54
msgid "Short name"
msgstr "Nom court"
#: agenda_culturel/models.py:50
#: agenda_culturel/models.py:54
msgid "Short name of the category"
msgstr "Nom court de la catégorie"
#: agenda_culturel/models.py:51
#: agenda_culturel/models.py:55
msgid "Color"
msgstr "Couleur"
#: agenda_culturel/models.py:51
#: agenda_culturel/models.py:55
msgid "Color used as background for the category"
msgstr "Couleur utilisée comme fond de la catégorie"
#: agenda_culturel/models.py:88 agenda_culturel/models.py:105
#: agenda_culturel/models.py:92 agenda_culturel/models.py:109
msgid "Category"
msgstr "Catégorie"
#: agenda_culturel/models.py:89
#: agenda_culturel/models.py:93
msgid "Categories"
msgstr "Catégories"
#: agenda_culturel/models.py:94
#: agenda_culturel/models.py:98
msgid "Published"
msgstr "Publié"
#: agenda_culturel/models.py:95
#: agenda_culturel/models.py:99
msgid "Draft"
msgstr "Brouillon"
#: agenda_culturel/models.py:96
#: agenda_culturel/models.py:100
msgid "Trash"
msgstr "Corbeille"
#: agenda_culturel/models.py:101
#: agenda_culturel/models.py:105
msgid "Title"
msgstr "Titre"
#: agenda_culturel/models.py:101
#: agenda_culturel/models.py:105
msgid "Short title"
msgstr "Titre court"
#: agenda_culturel/models.py:103
#: agenda_culturel/models.py:107 agenda_culturel/models.py:238
msgid "Status"
msgstr "Status"
#: agenda_culturel/models.py:105
#: agenda_culturel/models.py:109
msgid "Category of the event"
msgstr "Catégorie de l'événement"
#: agenda_culturel/models.py:107
#: agenda_culturel/models.py:111
msgid "Day of the event"
msgstr "Date de l'événement"
#: agenda_culturel/models.py:108
#: agenda_culturel/models.py:112
msgid "Starting time"
msgstr "Heure de début"
#: agenda_culturel/models.py:110
#: agenda_culturel/models.py:114
msgid "End day of the event"
msgstr "Fin de l'événement"
#: agenda_culturel/models.py:110
#: agenda_culturel/models.py:114
msgid "End day of the event, only required if different from the start day."
msgstr ""
"Date de fin de l'événement, uniquement nécessaire s'il est différent du "
"premier jour de l'événement"
#: agenda_culturel/models.py:111
#: agenda_culturel/models.py:115
msgid "Final time"
msgstr "Heure de fin"
#: agenda_culturel/models.py:113
#: agenda_culturel/models.py:117
msgid "Location"
msgstr "Localisation"
#: agenda_culturel/models.py:113
#: agenda_culturel/models.py:117
msgid "Address of the event"
msgstr "Adresse de l'événement"
#: agenda_culturel/models.py:115
#: agenda_culturel/models.py:119
msgid "Description"
msgstr "Description"
#: agenda_culturel/models.py:115
#: agenda_culturel/models.py:119
msgid "General description of the event"
msgstr "Description générale de l'événement"
#: agenda_culturel/models.py:117
#: agenda_culturel/models.py:121
msgid "Illustration (local image)"
msgstr "Illustration (image locale)"
#: agenda_culturel/models.py:117
#: agenda_culturel/models.py:121
msgid "Illustration image stored in the agenda server"
msgstr "Image d'illustration stockée sur le serveur de l'agenda"
#: agenda_culturel/models.py:119
#: agenda_culturel/models.py:123
msgid "Illustration"
msgstr "Illustration"
#: agenda_culturel/models.py:119
#: agenda_culturel/models.py:123
msgid "URL of the illustration image"
msgstr "URL de l'image illustrative"
#: agenda_culturel/models.py:120
#: agenda_culturel/models.py:124
msgid "Illustration description"
msgstr "Description de l'illustration"
#: agenda_culturel/models.py:120
#: agenda_culturel/models.py:124
msgid "Alternative text used by screen readers for the image"
msgstr "Texte alternatif utiliser par les lecteurs d'écrans pour l'image"
#: agenda_culturel/models.py:122
#: agenda_culturel/models.py:126
msgid "URLs"
msgstr "URLs"
#: agenda_culturel/models.py:122
#: agenda_culturel/models.py:126
msgid "List of all the urls where this event can be found."
msgstr "Liste de toutes les urls où l'événement peut être trouvé."
#: agenda_culturel/models.py:124
#: agenda_culturel/models.py:128
msgid "Tags"
msgstr "Étiquettes"
#: agenda_culturel/models.py:124
#: agenda_culturel/models.py:128
msgid "A list of tags that describe the event."
msgstr "Une liste d'étiquettes décrivant l'événement"
#: agenda_culturel/models.py:149
#: agenda_culturel/models.py:158
msgid "Event"
msgstr "Événement"
#: agenda_culturel/models.py:150
#: agenda_culturel/models.py:159
msgid "Events"
msgstr "Événements"
#: agenda_culturel/models.py:182
#: agenda_culturel/models.py:210
msgid "Subject"
msgstr "Sujet"
#: agenda_culturel/models.py:182
#: agenda_culturel/models.py:210
msgid "The subject of your message"
msgstr "Sujet de votre message"
#: agenda_culturel/models.py:183
#: agenda_culturel/models.py:211
msgid "Your name"
msgstr "Votre nom"
#: agenda_culturel/models.py:184
#: agenda_culturel/models.py:212
msgid "Email address"
msgstr "Adresse email"
#: agenda_culturel/models.py:184
#: agenda_culturel/models.py:212
msgid "Your email address"
msgstr "Votre adresse email"
#: agenda_culturel/models.py:185
#: agenda_culturel/models.py:213
msgid "Message"
msgstr "Message"
#: agenda_culturel/models.py:185
#: agenda_culturel/models.py:213
msgid "Your message"
msgstr "Votre message"
#: agenda_culturel/models.py:189 agenda_culturel/views.py:334
#: agenda_culturel/models.py:217 agenda_culturel/views.py:341
msgid "Closed"
msgstr "Fermé"
#: agenda_culturel/models.py:189
#: agenda_culturel/models.py:217
msgid "this message has been processed and no longer needs to be handled"
msgstr "Ce message a été traité et ne nécessite plus d'être pris en charge"
#: agenda_culturel/models.py:190
#: agenda_culturel/models.py:218
msgid "Comments"
msgstr "Commentaires"
#: agenda_culturel/models.py:190
#: agenda_culturel/models.py:218
msgid "Comments on the message from the moderation team"
msgstr "Commentaires sur ce message par l'équipe de modération"
#: agenda_culturel/settings/base.py:128
#: agenda_culturel/models.py:227
msgid "Running"
msgstr ""
#: agenda_culturel/models.py:228
msgid "Canceled"
msgstr "Annulé"
#: agenda_culturel/models.py:229
msgid "Success"
msgstr "Succès"
#: agenda_culturel/models.py:230
msgid "Failed"
msgstr "Erreur"
#: agenda_culturel/models.py:235
msgid "Source"
msgstr "Source"
#: agenda_culturel/models.py:235
msgid "URL of the source document"
msgstr "URL du document source"
#: agenda_culturel/models.py:236
msgid "Browsable url"
msgstr "URL navigable"
#: agenda_culturel/models.py:236
msgid "URL of the corresponding document that will be shown to visitors."
msgstr "URL correspondant au document et qui sera montrée aux visiteurs"
#: agenda_culturel/settings/base.py:134
msgid "English"
msgstr "anglais"
#: agenda_culturel/settings/base.py:129
#: agenda_culturel/settings/base.py:135
msgid "French"
msgstr "français"
#: agenda_culturel/views.py:182
#: agenda_culturel/views.py:188
msgid "The static content has been successfully updated."
msgstr "Le contenu statique a été modifié avec succès."
#: agenda_culturel/views.py:188
#: agenda_culturel/views.py:194
msgid "The event has been successfully modified."
msgstr "L'événement a été modifié avec succès."
#: agenda_culturel/views.py:199
#: agenda_culturel/views.py:205
msgid "The event has been successfully deleted."
msgstr "L'événement a été supprimé avec succès"
#: agenda_culturel/views.py:237
#: agenda_culturel/views.py:222
msgid "The status has been successfully modified."
msgstr "Le status a été modifié avec succès."
#: agenda_culturel/views.py:244
msgid "The event is saved."
msgstr "L'événement est enregistré."
#: agenda_culturel/views.py:240
#: agenda_culturel/views.py:247
msgid ""
"The event has been submitted and will be published as soon as it has been "
"validated by the moderation team."
@ -280,7 +308,7 @@ msgstr ""
"L'événement a été soumis et sera publié dès qu'il aura été validé par "
"l'équipe de modération."
#: agenda_culturel/views.py:270
#: agenda_culturel/views.py:277
msgid ""
"The event has been successfully extracted, and you can now submit it after "
"modifying it if necessary."
@ -288,7 +316,7 @@ msgstr ""
"L'événement a été extrait avec succès, vous pouvez maintenant le soumettre "
"après l'avoir modifié au besoin."
#: agenda_culturel/views.py:274
#: agenda_culturel/views.py:281
msgid ""
"Unable to extract an event from the proposed URL. Please use the form below "
"to submit the event."
@ -296,12 +324,12 @@ msgstr ""
"Impossible d'extraire un événement depuis l'URL proposée. Veuillez utiliser "
"le formulaire ci-dessous pour soumettre l'événement."
#: agenda_culturel/views.py:283
#: agenda_culturel/views.py:290
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."
#: agenda_culturel/views.py:287
#: agenda_culturel/views.py:294
msgid ""
"This URL has already been submitted, but has not been selected for "
"publication by the moderation team."
@ -309,26 +337,34 @@ msgstr ""
"Cette URL a déjà été soumise, mais n'a pas été retenue par l'équipe de "
"modération pour la publication."
#: agenda_culturel/views.py:289
#: agenda_culturel/views.py:296
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"
#: agenda_culturel/views.py:311
#: agenda_culturel/views.py:318
msgid "Your message has been sent successfully."
msgstr "L'événement a été supprimé avec succès"
#: agenda_culturel/views.py:319
#: agenda_culturel/views.py:326
msgid "The contact message properties has been successfully modified."
msgstr "Les propriétés du message de contact ont été modifié avec succès."
#: agenda_culturel/views.py:334
#: agenda_culturel/views.py:341
msgid "Open"
msgstr "Ouvert"
#: agenda_culturel/views.py:374
#: agenda_culturel/views.py:381
msgid "Search"
msgstr "Rechercher"
#: agenda_culturel/views.py:491
msgid "The import has been run successfully."
msgstr "L'import a été lancé avec succès"
#: agenda_culturel/views.py:507
msgid "The import has been canceled."
msgstr "L'import a été annulé"
msgid "Add another"
msgstr "Ajouter un autre"
@ -336,4 +372,4 @@ msgid "Browse..."
msgstr "Naviguer..."
msgid "No file selected."
msgstr "Pas de fichier sélectionné."
msgstr "Pas de fichier sélectionné."

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.7 on 2023-12-23 08:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0010_alter_contactmessage_comments'),
]
operations = [
migrations.CreateModel(
name='BatchImportation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(auto_now_add=True)),
('source', models.URLField(blank=True, help_text='URL of the source document', max_length=1024, null=True, verbose_name='Source')),
('browsable_url', models.URLField(blank=True, help_text='URL of the corresponding document that will be shown to visitors.', max_length=1024, null=True, verbose_name='Browsable url')),
('running', models.BooleanField(default=True, help_text='This batch importation is still running', verbose_name='Running')),
('success', models.BooleanField(default=False, help_text='This batch importation successfully finished', verbose_name='Success')),
],
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.7 on 2023-12-23 08:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0011_batchimportation'),
]
operations = [
migrations.RemoveField(
model_name='batchimportation',
name='running',
),
migrations.RemoveField(
model_name='batchimportation',
name='success',
),
migrations.AddField(
model_name='batchimportation',
name='status',
field=models.CharField(choices=[('running', 'Running'), ('canceled', 'Canceled'), ('success', 'Success'), ('failed', 'Failed')], default='running', max_length=20, verbose_name='Status'),
),
]

View File

@ -219,3 +219,20 @@ class ContactMessage(models.Model):
def nb_open_contactmessages():
return ContactMessage.objects.filter(closed=False).count()
class BatchImportation(models.Model):
class STATUS(models.TextChoices):
RUNNING = "running", _("Running")
CANCELED = "canceled", _("Canceled")
SUCCESS = "success", _("Success")
FAILED = "failed", _("Failed")
created_date = models.DateTimeField(auto_now_add=True)
source = models.URLField(verbose_name=_('Source'), help_text=_("URL of the source document"), max_length=1024, blank=True, null=True)
browsable_url = models.URLField(verbose_name=_('Browsable url'), help_text=_("URL of the corresponding document that will be shown to visitors."), max_length=1024, blank=True, null=True)
status = models.CharField(_("Status"), max_length=20, choices=STATUS.choices, default=STATUS.RUNNING)

View File

@ -0,0 +1,24 @@
{% extends "agenda_culturel/page.html" %}
{% load static %}
{% block title %}Importation par lot{% endblock %}
{% block content %}
<h1>Importation par lot</h1>
<article>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<p>
<label for="id_json">JSON (facultatif) :</label>
<textarea id="id_json" name="json" rows="10"></textarea>
<span class="helptext">JSON au format attendu pour l'import. Si le JSON est fourni ici, on ne lancera pas une récupération depuis l'URL donnée en paramètre.</span>
</p>
<input type="submit" value="Envoyer">
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}Supprimer {{ object.title }}{% endblock %}
{% block content %}
<article>
<header>
<h1>Annulation de l'import {{ object.id }} ({{ object.created_date }})</h1>
</header>
<form method="post">{% csrf_token %}
<p>Êtes-vous sûr·e de vouloir annuler l'importation «&nbsp;{{ object.id }}&nbsp;»&nbsp;?</p>
{{ form }}
<footer>
<div class="grid">
<a href="{{ cancel_url }}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Confirmer">
</div>
</footer>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}Importations par lot{% endblock %}
{% load utils_extra %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block content %}
<div class="grid two-columns">
<article>
<header>
<a class="slide-buttons" href="{% url 'add_import'%}" role="button">Nouvel import</a>
<h1>Importations par lot</h1>
</header>
<table role="grid">
<thead>
<tr>
<th>Identifiant</th>
<th>Date</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for obj in paginator_filter %}
<tr>
<td>{{ obj.id }}</a></td>
<td>{{ obj.created_date }}</td>
<td>{{ obj.status }}</td>
<td>{% if obj.status == "running" %}<a href="{% url 'cancel_import' obj.id %}">Annuler</a>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<footer>
<span>
{% if paginator_filter.has_previous %}
<a href="?page=1" role="button">&laquo; premier</a>
<a href="?page={{ paginator_filter.previous_page_number }}" role="button">précédent</a>
{% endif %}
<span>
Page {{ paginator_filter.number }} sur {{ paginator_filter.paginator.num_pages }}
</span>
{% if paginator_filter.has_next %}
<a href="?page={{ paginator_filter.next_page_number }}" role="button">suivant</a>
<a href="?page={{ paginator_filter.paginator.num_pages }}" role="button">dernier &raquo;</a>
{% endif %}
</span>
</footer>
</article>
{% include "agenda_culturel/side-nav.html" with current="imports" %}
</div>
{% endblock %}

View File

@ -10,6 +10,7 @@
<li><a {% if current == "moderation" %}class="selected" {% endif %}href="{% url 'moderation' %}">Derniers événements soumis</a>{% show_badges_events %}</li>
<li><a {% if current == "tags" %}class="selected" {% endif %}href="{% url 'view_all_tags' %}">Toutes les étiquettes</a></li>
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages %}</li>
<li><a {% if current == "imports" %}class="selected" {% endif %}href="{% url 'imports' %}">Importations par lot</a>{% show_badge_contactmessages %}</li>
</ul>
</nav>
</article>

View File

@ -34,6 +34,9 @@ urlpatterns = [
path('contact', ContactMessageCreateView.as_view(), name='contact'),
path('contactmessages', contactmessages, name='contactmessages'),
path('contactmessage/<int:pk>', ContactMessageUpdateView.as_view(), name='contactmessage'),
path("imports/", imports, name="imports"),
path("imports/add", BatchImportationCreateView.as_view(), name="add_import"),
path("imports/<int:pk>/cancel", cancel_import, name="cancel_import"),
]
if settings.DEBUG:

View File

@ -13,7 +13,7 @@ import urllib
from .forms import EventSubmissionForm, EventForm
from .models import Event, Category, StaticContent, ContactMessage
from .models import Event, Category, StaticContent, ContactMessage, BatchImportation
from django.utils import timezone
from enum import StrEnum
from datetime import date, timedelta
@ -219,6 +219,7 @@ def change_status_event(request, pk, status):
if request.method == 'POST':
event.status = Event.STATUS(status)
event.save(update_fields=["status"])
messages.success(request, _("The status has been successfully modified."))
if request.user.is_authenticated:
return HttpResponseRedirect(event.get_absolute_url())
@ -460,3 +461,54 @@ def event_search(request, full=False):
def event_search_full(request):
return event_search(request, True)
#########################
## batch importations
#########################
@login_required(login_url="/accounts/login/")
def imports(request):
paginator = Paginator(BatchImportation.objects.all().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/imports.html', {'paginator_filter': response} )
class BatchImportationCreateView(SuccessMessageMixin, LoginRequiredMixin, CreateView):
model = BatchImportation
fields = ['source', 'browsable_url']
success_url = reverse_lazy('imports')
success_message = _('The import has been run successfully.')
def form_valid(self, form):
# TODO run a celery script
return super().form_valid(form)
@login_required(login_url="/accounts/login/")
def cancel_import(request, pk):
import_process = get_object_or_404(BatchImportation, pk=pk)
if request.method == 'POST':
# TODO cancel the celery import
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})