* Ajout de l'import récurrent (manque la partie cron)
* Correction des textes en français. Fix #73
This commit is contained in:
parent
2bac3f29b5
commit
59c091a2f5
@ -1,249 +1,29 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
import urllib.request
|
|
||||||
import os
|
import os
|
||||||
from selenium import webdriver
|
|
||||||
from selenium.webdriver.chrome.service import Service
|
|
||||||
from selenium.webdriver.chrome.options import Options
|
|
||||||
import icalendar
|
|
||||||
from icalendar import vDatetime
|
|
||||||
from datetime import datetime, date
|
|
||||||
import json
|
import json
|
||||||
from bs4 import BeautifulSoup
|
import sys
|
||||||
import pickle
|
|
||||||
|
|
||||||
|
# getting the name of the directory
|
||||||
|
# where the this file is present.
|
||||||
|
current = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
class Downloader(ABC):
|
# Getting the parent directory name
|
||||||
|
# where the current directory is present.
|
||||||
|
parent = os.path.dirname(current)
|
||||||
|
|
||||||
def __init__(self):
|
# adding the parent directory to
|
||||||
pass
|
# the sys.path.
|
||||||
|
sys.path.append(parent)
|
||||||
|
|
||||||
@abstractmethod
|
from src.agenda_culturel.import_tasks.downloader import *
|
||||||
def download(self, url):
|
from src.agenda_culturel.import_tasks.extractor import *
|
||||||
pass
|
from src.agenda_culturel.import_tasks.importer import *
|
||||||
|
from src.agenda_culturel.import_tasks.extractor_ical import *
|
||||||
class SimpleDownloader(Downloader):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
|
|
||||||
def download(self, url):
|
|
||||||
print("Downloading {}".format(url))
|
|
||||||
|
|
||||||
try:
|
|
||||||
resource = urllib.request.urlopen(url)
|
|
||||||
data = resource.read().decode(resource.headers.get_content_charset())
|
|
||||||
return data
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ChromiumHeadlessDownloader(Downloader):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
options = Options()
|
|
||||||
options.add_argument("--headless=new")
|
|
||||||
service = Service("/usr/bin/chromedriver")
|
|
||||||
self.driver = webdriver.Chrome(service=service, options=options)
|
|
||||||
|
|
||||||
|
|
||||||
def download(self, url):
|
|
||||||
print("Download {}".format(url))
|
|
||||||
|
|
||||||
self.driver.get(url)
|
|
||||||
return driver.page_source
|
|
||||||
|
|
||||||
|
|
||||||
class Extractor(ABC):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.header = {}
|
|
||||||
self.events = []
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def extract(self, content, url, url_human = None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_header(self, url):
|
|
||||||
self.header["url"] = url
|
|
||||||
self.header["date"] = datetime.now()
|
|
||||||
|
|
||||||
def clear_events(self):
|
|
||||||
self.events = []
|
|
||||||
|
|
||||||
def add_event(self, title, category, start_day, location, description, tags, uuid, recurrences=None, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False):
|
|
||||||
if title is None:
|
|
||||||
print("ERROR: cannot import an event without name")
|
|
||||||
return
|
|
||||||
if start_day is None:
|
|
||||||
print("ERROR: cannot import an event without start day")
|
|
||||||
return
|
|
||||||
|
|
||||||
event = {
|
|
||||||
"title": title,
|
|
||||||
"category": category,
|
|
||||||
"start_day": start_day,
|
|
||||||
"uuid": uuid,
|
|
||||||
"location": location,
|
|
||||||
"description": description,
|
|
||||||
"tags": tags,
|
|
||||||
"published": published
|
|
||||||
}
|
|
||||||
if url_human is not None:
|
|
||||||
event["url_human"] = url_human
|
|
||||||
if start_time is not None:
|
|
||||||
event["start_time"] = start_time
|
|
||||||
if end_day is not None:
|
|
||||||
event["end_day"] = end_day
|
|
||||||
if end_time is not None:
|
|
||||||
event["end_time"] = end_time
|
|
||||||
|
|
||||||
if last_modified is not None:
|
|
||||||
event["last_modified"] = last_modified
|
|
||||||
|
|
||||||
if recurrences is not None:
|
|
||||||
event["recurrences"] = recurrences
|
|
||||||
|
|
||||||
self.events.append(event)
|
|
||||||
|
|
||||||
def default_value_if_exists(self, default_values, key):
|
|
||||||
return default_values[key] if default_values is not None and key in default_values else None
|
|
||||||
|
|
||||||
def get_structure(self):
|
|
||||||
return { "header": self.header, "events": self.events}
|
|
||||||
|
|
||||||
class ICALExtractor(Extractor):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def get_item_from_vevent(self, event, name, raw = False):
|
|
||||||
try:
|
|
||||||
r = event.decoded(name)
|
|
||||||
if raw:
|
|
||||||
return r
|
|
||||||
else:
|
|
||||||
return r.decode()
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_dt_item_from_vevent(self, event, name):
|
|
||||||
item = self.get_item_from_vevent(event, name, raw = True)
|
|
||||||
|
|
||||||
day = None
|
|
||||||
time = None
|
|
||||||
|
|
||||||
if item is not None:
|
|
||||||
if isinstance(item, datetime):
|
|
||||||
day = item.date()
|
|
||||||
time = item.time()
|
|
||||||
elif isinstance(item, date):
|
|
||||||
day = item
|
|
||||||
time = None
|
|
||||||
|
|
||||||
return day, time
|
|
||||||
|
|
||||||
|
|
||||||
def extract(self, content, url, url_human = None, default_values = None, published = False):
|
|
||||||
print("Extracting ical events from {}".format(url))
|
|
||||||
self.set_header(url)
|
|
||||||
self.clear_events()
|
|
||||||
self.uuids = {}
|
|
||||||
|
|
||||||
calendar = icalendar.Calendar.from_ical(content)
|
|
||||||
|
|
||||||
for event in calendar.walk('VEVENT'):
|
|
||||||
title = self.get_item_from_vevent(event, "SUMMARY")
|
|
||||||
category = self.default_value_if_exists(default_values, "category")
|
|
||||||
|
|
||||||
start_day, start_time = self.get_dt_item_from_vevent(event, "DTSTART")
|
|
||||||
|
|
||||||
end_day, end_time = self.get_dt_item_from_vevent(event, "DTEND")
|
|
||||||
|
|
||||||
location = self.get_item_from_vevent(event, "LOCATION")
|
|
||||||
if location is None:
|
|
||||||
location = self.default_value_if_exists(default_values, "location")
|
|
||||||
|
|
||||||
description = self.get_item_from_vevent(event, "DESCRIPTION")
|
|
||||||
if description is not None:
|
|
||||||
soup = BeautifulSoup(description)
|
|
||||||
delimiter = '\n'
|
|
||||||
for line_break in soup.findAll('br'):
|
|
||||||
line_break.replaceWith(delimiter)
|
|
||||||
description = soup.get_text()
|
|
||||||
|
|
||||||
last_modified = self.get_item_from_vevent(event, "LAST_MODIFIED")
|
|
||||||
|
|
||||||
uuid = self.get_item_from_vevent(event, "UID")
|
|
||||||
|
|
||||||
if uuid is not None:
|
|
||||||
if uuid in self.uuids:
|
|
||||||
self.uuids[uuid] += 1
|
|
||||||
uuid += ":{:04}".format(self.uuids[uuid] - 1)
|
|
||||||
else:
|
|
||||||
self.uuids[uuid] = 1
|
|
||||||
event_url = url + "#" + uuid
|
|
||||||
|
|
||||||
tags = self.default_value_if_exists(default_values, "tags")
|
|
||||||
|
|
||||||
last_modified = self.get_item_from_vevent(event, "LAST-MODIFIED", raw = True)
|
|
||||||
|
|
||||||
recurrence_entries = {}
|
|
||||||
for e in ["RRULE", "EXRULE", "EXDATE", "RDATE"]:
|
|
||||||
i = self.get_item_from_vevent(event, e, raw = True)
|
|
||||||
if i is not None:
|
|
||||||
recurrence_entries[e] = i
|
|
||||||
|
|
||||||
if start_day is not None and len(recurrence_entries) != 0:
|
|
||||||
recurrences = ""
|
|
||||||
|
|
||||||
for k, r in recurrence_entries.items():
|
|
||||||
if isinstance(r, list):
|
|
||||||
recurrences += "\n".join([k + ":" + e.to_ical().decode() for e in r]) + "\n"
|
|
||||||
else:
|
|
||||||
recurrences += k + ":" + r.to_ical().decode() + "\n"
|
|
||||||
else:
|
|
||||||
recurrences = None
|
|
||||||
|
|
||||||
|
|
||||||
self.add_event(title, category, start_day, location, description, tags, recurrences=recurrences, uuid=event_url, url_human=url_human, start_time=start_time, end_day=end_day, end_time=end_time, last_modified=last_modified, published=published)
|
|
||||||
|
|
||||||
return self.get_structure()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class URL2Events:
|
|
||||||
|
|
||||||
def __init__(self, downloader, extractor):
|
|
||||||
|
|
||||||
self.downloader = downloader
|
|
||||||
self.extractor = extractor
|
|
||||||
|
|
||||||
def process(self, url, url_human = None, cache = None, default_values = None, published = False):
|
|
||||||
|
|
||||||
if cache and os.path.exists(cache):
|
|
||||||
print("Loading cache ({})".format(cache))
|
|
||||||
with open(cache) as f:
|
|
||||||
content = "\n".join(f.readlines())
|
|
||||||
else:
|
|
||||||
content = self.downloader.download(url)
|
|
||||||
|
|
||||||
if cache:
|
|
||||||
print("Saving cache ({})".format(cache))
|
|
||||||
dir = os.path.dirname(cache)
|
|
||||||
if dir != "" and not os.path.exists(dir):
|
|
||||||
os.makedirs(dir)
|
|
||||||
with open(cache, "w") as text_file:
|
|
||||||
text_file.write(content)
|
|
||||||
|
|
||||||
return self.extractor.extract(content, url, url_human, default_values, published)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django import forms
|
from django import forms
|
||||||
from .models import Event, Category, StaticContent, DuplicatedEvents, BatchImportation
|
from .models import Event, Category, StaticContent, DuplicatedEvents, BatchImportation, RecurrentImport
|
||||||
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
|
||||||
@ -10,6 +10,7 @@ admin.site.register(Category)
|
|||||||
admin.site.register(StaticContent)
|
admin.site.register(StaticContent)
|
||||||
admin.site.register(DuplicatedEvents)
|
admin.site.register(DuplicatedEvents)
|
||||||
admin.site.register(BatchImportation)
|
admin.site.register(BatchImportation)
|
||||||
|
admin.site.register(RecurrentImport)
|
||||||
|
|
||||||
|
|
||||||
class URLWidget(DynamicArrayWidget):
|
class URLWidget(DynamicArrayWidget):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
@ -6,6 +7,13 @@ from celery.utils.log import get_task_logger
|
|||||||
|
|
||||||
from .extractors import ExtractorAllURLs
|
from .extractors import ExtractorAllURLs
|
||||||
|
|
||||||
|
from .import_tasks.downloader import *
|
||||||
|
from .import_tasks.extractor import *
|
||||||
|
from .import_tasks.importer import *
|
||||||
|
from .import_tasks.extractor_ical import *
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Set the default Django settings module for the 'celery' program.
|
# Set the default Django settings module for the 'celery' program.
|
||||||
APP_ENV = os.getenv("APP_ENV", "dev")
|
APP_ENV = os.getenv("APP_ENV", "dev")
|
||||||
@ -44,14 +52,20 @@ def close_import_task(taskid, success, error_message, importer):
|
|||||||
|
|
||||||
@app.task(bind=True)
|
@app.task(bind=True)
|
||||||
def import_events_from_json(self, json):
|
def import_events_from_json(self, json):
|
||||||
from agenda_culturel.models import Event
|
from agenda_culturel.models import Event, BatchImportation
|
||||||
from .importation import EventsImporter
|
from .importation import EventsImporter
|
||||||
|
|
||||||
|
# create a batch importation
|
||||||
|
importation = BatchImportation(celery_id=self.request.id)
|
||||||
|
# save batch importation
|
||||||
|
importation.save()
|
||||||
|
|
||||||
|
|
||||||
logger.info("Import events from json: {}".format(self.request.id))
|
logger.info("Import events from json: {}".format(self.request.id))
|
||||||
|
|
||||||
importer = EventsImporter(self.request.id)
|
importer = EventsImporter(self.request.id)
|
||||||
|
|
||||||
# try:
|
#try:
|
||||||
success, error_message = importer.import_events(json)
|
success, error_message = importer.import_events(json)
|
||||||
|
|
||||||
# finally, close task
|
# finally, close task
|
||||||
@ -62,25 +76,55 @@ def import_events_from_json(self, json):
|
|||||||
|
|
||||||
|
|
||||||
@app.task(bind=True)
|
@app.task(bind=True)
|
||||||
def import_events_from_url(self, source, browsable_url):
|
def run_recurrent_import(self, pk):
|
||||||
from agenda_culturel.models import Event
|
from agenda_culturel.models import RecurrentImport, BatchImportation
|
||||||
from .importation import EventsImporter
|
from .importation import EventsImporter
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
logger.info("Import events from url: {} {}".format(source, self.request.id))
|
logger.info("Run recurrent import: {}".format(self.request.id))
|
||||||
|
|
||||||
# first get json
|
# get the recurrent import
|
||||||
# TODO
|
rimport = RecurrentImport.objects.get(pk=pk)
|
||||||
|
|
||||||
# then import
|
# create a batch importation
|
||||||
# importer = EventsImporter(self.request.id)
|
importation = BatchImportation(recurrentImport=rimport, celery_id=self.request.id)
|
||||||
# success, error_message = importer.import_events(json)
|
# save batch importation
|
||||||
|
importation.save()
|
||||||
|
|
||||||
|
# create an importer
|
||||||
|
importer = EventsImporter(self.request.id)
|
||||||
|
|
||||||
success = True
|
# prepare downloading and extracting processes
|
||||||
error_message = ""
|
downloader = SimpleDownloader() if rimport.downloader == RecurrentImport.DOWNLOADER.SIMPLE else ChromiumHeadlessDownloader()
|
||||||
|
extractor = ICALExtractor() if rimport.processor == RecurrentImport.PROCESSOR.ICAL else None
|
||||||
|
|
||||||
# finally, close task
|
if extractor is None:
|
||||||
close_import_task(self.request.id, success, error_message)
|
logger.error("Unknown extractor")
|
||||||
|
close_import_task(self.request.id, False, "Unknown extractor", importer)
|
||||||
|
|
||||||
|
# set parameters
|
||||||
|
u2e = URL2Events(downloader, extractor)
|
||||||
|
url = rimport.source
|
||||||
|
browsable_url = rimport.browsable_url
|
||||||
|
category = str(rimport.defaultCategory)
|
||||||
|
location = rimport.defaultLocation
|
||||||
|
tags = rimport.defaultTags
|
||||||
|
|
||||||
|
try:
|
||||||
|
# get events from website
|
||||||
|
events = u2e.process(url, browsable_url, default_values = {"category": category, "location": location, "tags": tags}, published = True)
|
||||||
|
|
||||||
|
# convert it to json
|
||||||
|
json_events = json.dumps(events, default=str)
|
||||||
|
|
||||||
|
# import events (from json)
|
||||||
|
success, error_message = importer.import_events(json_events)
|
||||||
|
|
||||||
|
# finally, close task
|
||||||
|
close_import_task(self.request.id, success, error_message, importer)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
close_import_task(self.request.id, False, e, importer)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ from django.forms import ModelForm, ValidationError, TextInput, Form, URLField,
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
|
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
|
||||||
|
|
||||||
from .models import Event, BatchImportation
|
from .models import Event, BatchImportation, RecurrentImport
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from string import ascii_uppercase as auc
|
from string import ascii_uppercase as auc
|
||||||
from .templatetags.utils_extra import int_to_abc
|
from .templatetags.utils_extra import int_to_abc
|
||||||
@ -25,6 +25,11 @@ class DynamicArrayWidgetURLs(DynamicArrayWidget):
|
|||||||
class DynamicArrayWidgetTags(DynamicArrayWidget):
|
class DynamicArrayWidgetTags(DynamicArrayWidget):
|
||||||
template_name = "agenda_culturel/widgets/widget-tags.html"
|
template_name = "agenda_culturel/widgets/widget-tags.html"
|
||||||
|
|
||||||
|
class RecurrentImportForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = RecurrentImport
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
class EventForm(ModelForm):
|
class EventForm(ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -75,26 +80,8 @@ class EventForm(ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BatchImportationForm(ModelForm):
|
class BatchImportationForm(Form):
|
||||||
|
json = CharField(label="JSON", widget=Textarea(attrs={"rows":"10"}), help_text=_("JSON in the format expected for the import."), required=True)
|
||||||
json = CharField(label="JSON (facultatif)", widget=Textarea(attrs={"rows":"10"}), help_text=_("JSON in the format expected for the import. If the JSON is provided here, we will ignore the URLs given above, and use the information provided by the json without importing any additional events from the URL."), required=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = BatchImportation
|
|
||||||
fields = ['source', 'browsable_url']
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
json = cleaned_data.get("json")
|
|
||||||
source = cleaned_data.get("source")
|
|
||||||
browsable_url = cleaned_data.get("browsable_url")
|
|
||||||
|
|
||||||
if (not json or json == "") and (not source or source == "") and (not browsable_url or browsable_url == ""):
|
|
||||||
raise ValidationError(_("You need to fill in either the json or the source possibly supplemented by the navigable URL."))
|
|
||||||
|
|
||||||
# Always return a value to use as the new cleaned data, even if
|
|
||||||
# this method didn't change it.
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
class FixDuplicates(Form):
|
class FixDuplicates(Form):
|
||||||
|
50
src/agenda_culturel/import_tasks/downloader.py
Normal file
50
src/agenda_culturel/import_tasks/downloader.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
import urllib.request
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Downloader(ABC):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def download(self, url):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SimpleDownloader(Downloader):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def download(self, url):
|
||||||
|
print("Downloading {}".format(url))
|
||||||
|
|
||||||
|
try:
|
||||||
|
resource = urllib.request.urlopen(url)
|
||||||
|
data = resource.read().decode(resource.headers.get_content_charset())
|
||||||
|
return data
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ChromiumHeadlessDownloader(Downloader):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
options = Options()
|
||||||
|
options.add_argument("--headless=new")
|
||||||
|
service = Service("/usr/bin/chromedriver")
|
||||||
|
self.driver = webdriver.Chrome(service=service, options=options)
|
||||||
|
|
||||||
|
|
||||||
|
def download(self, url):
|
||||||
|
print("Download {}".format(url))
|
||||||
|
|
||||||
|
self.driver.get(url)
|
||||||
|
return driver.page_source
|
62
src/agenda_culturel/import_tasks/extractor.py
Normal file
62
src/agenda_culturel/import_tasks/extractor.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Extractor(ABC):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.header = {}
|
||||||
|
self.events = []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract(self, content, url, url_human = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_header(self, url):
|
||||||
|
self.header["url"] = url
|
||||||
|
self.header["date"] = datetime.now()
|
||||||
|
|
||||||
|
def clear_events(self):
|
||||||
|
self.events = []
|
||||||
|
|
||||||
|
def add_event(self, title, category, start_day, location, description, tags, uuid, recurrences=None, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False):
|
||||||
|
if title is None:
|
||||||
|
print("ERROR: cannot import an event without name")
|
||||||
|
return
|
||||||
|
if start_day is None:
|
||||||
|
print("ERROR: cannot import an event without start day")
|
||||||
|
return
|
||||||
|
|
||||||
|
event = {
|
||||||
|
"title": title,
|
||||||
|
"category": category,
|
||||||
|
"start_day": start_day,
|
||||||
|
"uuid": uuid,
|
||||||
|
"location": location,
|
||||||
|
"description": description,
|
||||||
|
"tags": tags,
|
||||||
|
"published": published
|
||||||
|
}
|
||||||
|
if url_human is not None:
|
||||||
|
event["url_human"] = url_human
|
||||||
|
if start_time is not None:
|
||||||
|
event["start_time"] = start_time
|
||||||
|
if end_day is not None:
|
||||||
|
event["end_day"] = end_day
|
||||||
|
if end_time is not None:
|
||||||
|
event["end_time"] = end_time
|
||||||
|
|
||||||
|
if last_modified is not None:
|
||||||
|
event["last_modified"] = last_modified
|
||||||
|
|
||||||
|
if recurrences is not None:
|
||||||
|
event["recurrences"] = recurrences
|
||||||
|
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
|
def default_value_if_exists(self, default_values, key):
|
||||||
|
return default_values[key] if default_values is not None and key in default_values else None
|
||||||
|
|
||||||
|
def get_structure(self):
|
||||||
|
return { "header": self.header, "events": self.events}
|
110
src/agenda_culturel/import_tasks/extractor_ical.py
Normal file
110
src/agenda_culturel/import_tasks/extractor_ical.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import icalendar
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from icalendar import vDatetime
|
||||||
|
from datetime import datetime, date
|
||||||
|
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
||||||
|
|
||||||
|
from .extractor import *
|
||||||
|
|
||||||
|
|
||||||
|
class ICALExtractor(Extractor):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def get_item_from_vevent(self, event, name, raw = False):
|
||||||
|
try:
|
||||||
|
r = event.decoded(name)
|
||||||
|
if raw:
|
||||||
|
return r
|
||||||
|
else:
|
||||||
|
return r.decode()
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_dt_item_from_vevent(self, event, name):
|
||||||
|
item = self.get_item_from_vevent(event, name, raw = True)
|
||||||
|
|
||||||
|
day = None
|
||||||
|
time = None
|
||||||
|
|
||||||
|
if item is not None:
|
||||||
|
if isinstance(item, datetime):
|
||||||
|
day = item.date()
|
||||||
|
time = item.time()
|
||||||
|
elif isinstance(item, date):
|
||||||
|
day = item
|
||||||
|
time = None
|
||||||
|
|
||||||
|
return day, time
|
||||||
|
|
||||||
|
|
||||||
|
def extract(self, content, url, url_human = None, default_values = None, published = False):
|
||||||
|
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
||||||
|
|
||||||
|
print("Extracting ical events from {}".format(url))
|
||||||
|
self.set_header(url)
|
||||||
|
self.clear_events()
|
||||||
|
self.uuids = {}
|
||||||
|
|
||||||
|
calendar = icalendar.Calendar.from_ical(content)
|
||||||
|
|
||||||
|
for event in calendar.walk('VEVENT'):
|
||||||
|
title = self.get_item_from_vevent(event, "SUMMARY")
|
||||||
|
category = self.default_value_if_exists(default_values, "category")
|
||||||
|
|
||||||
|
start_day, start_time = self.get_dt_item_from_vevent(event, "DTSTART")
|
||||||
|
|
||||||
|
end_day, end_time = self.get_dt_item_from_vevent(event, "DTEND")
|
||||||
|
|
||||||
|
location = self.get_item_from_vevent(event, "LOCATION")
|
||||||
|
if location is None:
|
||||||
|
location = self.default_value_if_exists(default_values, "location")
|
||||||
|
|
||||||
|
description = self.get_item_from_vevent(event, "DESCRIPTION")
|
||||||
|
if description is not None:
|
||||||
|
soup = BeautifulSoup(description, features="lxml")
|
||||||
|
delimiter = '\n'
|
||||||
|
for line_break in soup.findAll('br'):
|
||||||
|
line_break.replaceWith(delimiter)
|
||||||
|
description = soup.get_text()
|
||||||
|
|
||||||
|
last_modified = self.get_item_from_vevent(event, "LAST_MODIFIED")
|
||||||
|
|
||||||
|
uuid = self.get_item_from_vevent(event, "UID")
|
||||||
|
|
||||||
|
if uuid is not None:
|
||||||
|
if uuid in self.uuids:
|
||||||
|
self.uuids[uuid] += 1
|
||||||
|
uuid += ":{:04}".format(self.uuids[uuid] - 1)
|
||||||
|
else:
|
||||||
|
self.uuids[uuid] = 1
|
||||||
|
event_url = url + "#" + uuid
|
||||||
|
|
||||||
|
tags = self.default_value_if_exists(default_values, "tags")
|
||||||
|
|
||||||
|
last_modified = self.get_item_from_vevent(event, "LAST-MODIFIED", raw = True)
|
||||||
|
|
||||||
|
recurrence_entries = {}
|
||||||
|
for e in ["RRULE", "EXRULE", "EXDATE", "RDATE"]:
|
||||||
|
i = self.get_item_from_vevent(event, e, raw = True)
|
||||||
|
if i is not None:
|
||||||
|
recurrence_entries[e] = i
|
||||||
|
|
||||||
|
if start_day is not None and len(recurrence_entries) != 0:
|
||||||
|
recurrences = ""
|
||||||
|
|
||||||
|
for k, r in recurrence_entries.items():
|
||||||
|
if isinstance(r, list):
|
||||||
|
recurrences += "\n".join([k + ":" + e.to_ical().decode() for e in r]) + "\n"
|
||||||
|
else:
|
||||||
|
recurrences += k + ":" + r.to_ical().decode() + "\n"
|
||||||
|
else:
|
||||||
|
recurrences = None
|
||||||
|
|
||||||
|
if title is not None:
|
||||||
|
self.add_event(title, category, start_day, location, description, tags, recurrences=recurrences, uuid=event_url, url_human=url_human, start_time=start_time, end_day=end_day, end_time=end_time, last_modified=last_modified, published=published)
|
||||||
|
|
||||||
|
return self.get_structure()
|
||||||
|
|
31
src/agenda_culturel/import_tasks/importer.py
Normal file
31
src/agenda_culturel/import_tasks/importer.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from .downloader import *
|
||||||
|
from .extractor import *
|
||||||
|
|
||||||
|
|
||||||
|
class URL2Events:
|
||||||
|
|
||||||
|
def __init__(self, downloader, extractor):
|
||||||
|
|
||||||
|
self.downloader = downloader
|
||||||
|
self.extractor = extractor
|
||||||
|
|
||||||
|
def process(self, url, url_human = None, cache = None, default_values = None, published = False):
|
||||||
|
|
||||||
|
if cache and os.path.exists(cache):
|
||||||
|
print("Loading cache ({})".format(cache))
|
||||||
|
with open(cache) as f:
|
||||||
|
content = "\n".join(f.readlines())
|
||||||
|
else:
|
||||||
|
content = self.downloader.download(url)
|
||||||
|
|
||||||
|
if cache:
|
||||||
|
print("Saving cache ({})".format(cache))
|
||||||
|
dir = os.path.dirname(cache)
|
||||||
|
if dir != "" and not os.path.exists(dir):
|
||||||
|
os.makedirs(dir)
|
||||||
|
with open(cache, "w") as text_file:
|
||||||
|
text_file.write(content)
|
||||||
|
|
||||||
|
return self.extractor.extract(content, url, url_human, default_values, published)
|
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: agenda_culturel\n"
|
"Project-Id-Version: agenda_culturel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-12-29 15:35+0000\n"
|
"POT-Creation-Date: 2024-01-26 12:27+0000\n"
|
||||||
"PO-Revision-Date: 2023-10-29 14:16+0000\n"
|
"PO-Revision-Date: 2023-10-29 14:16+0000\n"
|
||||||
"Last-Translator: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
|
"Last-Translator: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
|
||||||
"Language-Team: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
|
"Language-Team: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
|
||||||
@ -17,340 +17,395 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
#: agenda_culturel/forms.py:38
|
#: agenda_culturel/forms.py:62
|
||||||
msgid "The end date must be after the start date."
|
msgid "The end date must be after the start date."
|
||||||
msgstr "La date de fin doit être après la date de début."
|
msgstr "La date de fin doit être après la date de début."
|
||||||
|
|
||||||
#: agenda_culturel/forms.py:53
|
#: agenda_culturel/forms.py:77
|
||||||
msgid "The end time cannot be earlier than the start time."
|
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."
|
msgstr "L'heure de fin ne peut pas être avant l'heure de début."
|
||||||
|
|
||||||
#: agenda_culturel/forms.py:61
|
#: agenda_culturel/forms.py:84
|
||||||
msgid ""
|
msgid "JSON in the format expected for the import."
|
||||||
"JSON in the format expected for the import. If the JSON is provided here, we "
|
msgstr "JSON dans le format attendu pour l'import"
|
||||||
"will ignore the URLs given above, and use the information provided by the "
|
|
||||||
"json without importing any additional events from the URL."
|
|
||||||
msgstr ""
|
|
||||||
"JSON au format attendu pour l'import. Si le JSON est fourni ici, on ignorera "
|
|
||||||
"les URL données au dessus, et on utilisera les informations fournies par le "
|
|
||||||
"json sans réaliser d'importation supplémentaire d'événements depuis l'URL."
|
|
||||||
|
|
||||||
#: agenda_culturel/forms.py:74
|
#: agenda_culturel/models.py:32 agenda_culturel/models.py:61
|
||||||
msgid ""
|
#: agenda_culturel/models.py:695
|
||||||
"You need to fill in either the json or the source possibly supplemented by "
|
|
||||||
"the navigable URL."
|
|
||||||
msgstr ""
|
|
||||||
"Vous devez renseigner soit le json soit la source éventuellement complétée "
|
|
||||||
"de l'URL navigable."
|
|
||||||
|
|
||||||
#: agenda_culturel/models.py:26 agenda_culturel/models.py:55
|
|
||||||
#: agenda_culturel/models.py:465
|
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "Nom"
|
msgstr "Nom"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:26 agenda_culturel/models.py:55
|
#: agenda_culturel/models.py:32 agenda_culturel/models.py:61
|
||||||
msgid "Category name"
|
msgid "Category name"
|
||||||
msgstr "Nom de la catégorie"
|
msgstr "Nom de la catégorie"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:27
|
#: agenda_culturel/models.py:33
|
||||||
msgid "Content"
|
msgid "Content"
|
||||||
msgstr "Contenu"
|
msgstr "Contenu"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:27
|
#: agenda_culturel/models.py:33
|
||||||
msgid "Text as shown to the visitors"
|
msgid "Text as shown to the visitors"
|
||||||
msgstr "Text tel que présenté aux visiteureuses"
|
msgstr "Text tel que présenté aux visiteureuses"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:28
|
#: agenda_culturel/models.py:34
|
||||||
msgid "URL path"
|
msgid "URL path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: agenda_culturel/models.py:28
|
#: agenda_culturel/models.py:34
|
||||||
msgid "URL path where the content is included."
|
msgid "URL path where the content is included."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: agenda_culturel/models.py:56
|
#: agenda_culturel/models.py:62
|
||||||
msgid "Alternative Name"
|
msgid "Alternative Name"
|
||||||
msgstr "Nom alternatif"
|
msgstr "Nom alternatif"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:56
|
#: agenda_culturel/models.py:62
|
||||||
msgid "Alternative name used with a time period"
|
msgid "Alternative name used with a time period"
|
||||||
msgstr "Nom alternatif utilisé avec une période de temps"
|
msgstr "Nom alternatif utilisé avec une période de temps"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:57
|
#: agenda_culturel/models.py:63
|
||||||
msgid "Short name"
|
msgid "Short name"
|
||||||
msgstr "Nom court"
|
msgstr "Nom court"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:57
|
#: agenda_culturel/models.py:63
|
||||||
msgid "Short name of the category"
|
msgid "Short name of the category"
|
||||||
msgstr "Nom court de la catégorie"
|
msgstr "Nom court de la catégorie"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:58
|
#: agenda_culturel/models.py:64
|
||||||
msgid "Color"
|
msgid "Color"
|
||||||
msgstr "Couleur"
|
msgstr "Couleur"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:58
|
#: agenda_culturel/models.py:64
|
||||||
msgid "Color used as background for the category"
|
msgid "Color used as background for the category"
|
||||||
msgstr "Couleur utilisée comme fond de la catégorie"
|
msgstr "Couleur utilisée comme fond de la catégorie"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:95 agenda_culturel/models.py:142
|
#: agenda_culturel/models.py:101 agenda_culturel/models.py:168
|
||||||
|
#: agenda_culturel/models.py:733
|
||||||
msgid "Category"
|
msgid "Category"
|
||||||
msgstr "Catégorie"
|
msgstr "Catégorie"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:96
|
#: agenda_culturel/models.py:102
|
||||||
msgid "Categories"
|
msgid "Categories"
|
||||||
msgstr "Catégories"
|
msgstr "Catégories"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:130
|
#: agenda_culturel/models.py:153
|
||||||
msgid "Published"
|
msgid "Published"
|
||||||
msgstr "Publié"
|
msgstr "Publié"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:131
|
#: agenda_culturel/models.py:154
|
||||||
msgid "Draft"
|
msgid "Draft"
|
||||||
msgstr "Brouillon"
|
msgstr "Brouillon"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:132
|
#: agenda_culturel/models.py:155
|
||||||
msgid "Trash"
|
msgid "Trash"
|
||||||
msgstr "Corbeille"
|
msgstr "Corbeille"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:138
|
#: agenda_culturel/models.py:164
|
||||||
msgid "Title"
|
msgid "Title"
|
||||||
msgstr "Titre"
|
msgstr "Titre"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:138
|
#: agenda_culturel/models.py:164
|
||||||
msgid "Short title"
|
msgid "Short title"
|
||||||
msgstr "Titre court"
|
msgstr "Titre court"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:140 agenda_culturel/models.py:492
|
#: agenda_culturel/models.py:166 agenda_culturel/models.py:756
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr "Status"
|
msgstr "Status"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:142
|
#: agenda_culturel/models.py:168
|
||||||
msgid "Category of the event"
|
msgid "Category of the event"
|
||||||
msgstr "Catégorie de l'événement"
|
msgstr "Catégorie de l'événement"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:144
|
#: agenda_culturel/models.py:170
|
||||||
msgid "Day of the event"
|
msgid "Day of the event"
|
||||||
msgstr "Date de l'événement"
|
msgstr "Date de l'événement"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:145
|
#: agenda_culturel/models.py:171
|
||||||
msgid "Starting time"
|
msgid "Starting time"
|
||||||
msgstr "Heure de début"
|
msgstr "Heure de début"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:147
|
#: agenda_culturel/models.py:173
|
||||||
msgid "End day of the event"
|
msgid "End day of the event"
|
||||||
msgstr "Fin de l'événement"
|
msgstr "Fin de l'événement"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:147
|
#: agenda_culturel/models.py:173
|
||||||
msgid "End day of the event, only required if different from the start day."
|
msgid "End day of the event, only required if different from the start day."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Date de fin de l'événement, uniquement nécessaire s'il est différent du "
|
"Date de fin de l'événement, uniquement nécessaire s'il est différent du "
|
||||||
"premier jour de l'événement"
|
"premier jour de l'événement"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:148
|
#: agenda_culturel/models.py:174
|
||||||
msgid "Final time"
|
msgid "Final time"
|
||||||
msgstr "Heure de fin"
|
msgstr "Heure de fin"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:150
|
#: agenda_culturel/models.py:176
|
||||||
|
msgid "Recurrence"
|
||||||
|
msgstr "Récurrence"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:178 agenda_culturel/models.py:731
|
||||||
msgid "Location"
|
msgid "Location"
|
||||||
msgstr "Localisation"
|
msgstr "Localisation"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:150
|
#: agenda_culturel/models.py:178
|
||||||
msgid "Address of the event"
|
msgid "Address of the event"
|
||||||
msgstr "Adresse de l'événement"
|
msgstr "Adresse de l'événement"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:152
|
#: agenda_culturel/models.py:180
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Description"
|
msgstr "Description"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:152
|
#: agenda_culturel/models.py:180
|
||||||
msgid "General description of the event"
|
msgid "General description of the event"
|
||||||
msgstr "Description générale de l'événement"
|
msgstr "Description générale de l'événement"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:154
|
#: agenda_culturel/models.py:182
|
||||||
msgid "Illustration (local image)"
|
msgid "Illustration (local image)"
|
||||||
msgstr "Illustration (image locale)"
|
msgstr "Illustration (image locale)"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:154
|
#: agenda_culturel/models.py:182
|
||||||
msgid "Illustration image stored in the agenda server"
|
msgid "Illustration image stored in the agenda server"
|
||||||
msgstr "Image d'illustration stockée sur le serveur de l'agenda"
|
msgstr "Image d'illustration stockée sur le serveur de l'agenda"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:156
|
#: agenda_culturel/models.py:184
|
||||||
msgid "Illustration"
|
msgid "Illustration"
|
||||||
msgstr "Illustration"
|
msgstr "Illustration"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:156
|
#: agenda_culturel/models.py:184
|
||||||
msgid "URL of the illustration image"
|
msgid "URL of the illustration image"
|
||||||
msgstr "URL de l'image illustrative"
|
msgstr "URL de l'image illustrative"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:157
|
#: agenda_culturel/models.py:185
|
||||||
msgid "Illustration description"
|
msgid "Illustration description"
|
||||||
msgstr "Description de l'illustration"
|
msgstr "Description de l'illustration"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:157
|
#: agenda_culturel/models.py:185
|
||||||
msgid "Alternative text used by screen readers for the image"
|
msgid "Alternative text used by screen readers for the image"
|
||||||
msgstr "Texte alternatif utiliser par les lecteurs d'écrans pour l'image"
|
msgstr "Texte alternatif utiliser par les lecteurs d'écrans pour l'image"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:159
|
#: agenda_culturel/models.py:187
|
||||||
|
msgid "Importation source"
|
||||||
|
msgstr "Source d'importation"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:187
|
||||||
|
msgid "Importation source used to detect removed entries."
|
||||||
|
msgstr "Source d'importation utilisée pour détecter les éléments supprimés/"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:188
|
||||||
msgid "UUIDs"
|
msgid "UUIDs"
|
||||||
msgstr "UUIDs"
|
msgstr "UUIDs"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:159
|
#: agenda_culturel/models.py:188
|
||||||
msgid "UUIDs from import to detect duplicated entries."
|
msgid "UUIDs from import to detect duplicated entries."
|
||||||
msgstr "UUIDs utilisés pendant l'import pour détecter les entrées dupliquées"
|
msgstr "UUIDs utilisés pendant l'import pour détecter les entrées dupliquées"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:160
|
#: agenda_culturel/models.py:189
|
||||||
msgid "URLs"
|
msgid "URLs"
|
||||||
msgstr "URLs"
|
msgstr "URLs"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:160
|
#: agenda_culturel/models.py:189
|
||||||
msgid "List of all the urls where this event can be found."
|
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é."
|
msgstr "Liste de toutes les urls où l'événement peut être trouvé."
|
||||||
|
|
||||||
#: agenda_culturel/models.py:162
|
#: agenda_culturel/models.py:191
|
||||||
msgid "Tags"
|
msgid "Tags"
|
||||||
msgstr "Étiquettes"
|
msgstr "Étiquettes"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:162
|
#: agenda_culturel/models.py:191
|
||||||
msgid "A list of tags that describe the event."
|
msgid "A list of tags that describe the event."
|
||||||
msgstr "Une liste d'étiquettes décrivant l'événement"
|
msgstr "Une liste d'étiquettes décrivant l'événement"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:164
|
#: agenda_culturel/models.py:193
|
||||||
msgid "Possibly duplicated"
|
msgid "Possibly duplicated"
|
||||||
msgstr "Possibles doublons"
|
msgstr "Possibles doublons"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:194
|
#: agenda_culturel/models.py:234
|
||||||
msgid "Event"
|
msgid "Event"
|
||||||
msgstr "Événement"
|
msgstr "Événement"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:195
|
#: agenda_culturel/models.py:235
|
||||||
msgid "Events"
|
msgid "Events"
|
||||||
msgstr "Événements"
|
msgstr "Événements"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:464
|
#: agenda_culturel/models.py:694
|
||||||
msgid "Subject"
|
msgid "Subject"
|
||||||
msgstr "Sujet"
|
msgstr "Sujet"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:464
|
#: agenda_culturel/models.py:694
|
||||||
msgid "The subject of your message"
|
msgid "The subject of your message"
|
||||||
msgstr "Sujet de votre message"
|
msgstr "Sujet de votre message"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:465
|
#: agenda_culturel/models.py:695
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "Votre nom"
|
msgstr "Votre nom"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:466
|
#: agenda_culturel/models.py:696
|
||||||
msgid "Email address"
|
msgid "Email address"
|
||||||
msgstr "Adresse email"
|
msgstr "Adresse email"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:466
|
#: agenda_culturel/models.py:696
|
||||||
msgid "Your email address"
|
msgid "Your email address"
|
||||||
msgstr "Votre adresse email"
|
msgstr "Votre adresse email"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:467
|
#: agenda_culturel/models.py:697
|
||||||
msgid "Message"
|
msgid "Message"
|
||||||
msgstr "Message"
|
msgstr "Message"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:467
|
#: agenda_culturel/models.py:697
|
||||||
msgid "Your message"
|
msgid "Your message"
|
||||||
msgstr "Votre message"
|
msgstr "Votre message"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:471 agenda_culturel/views.py:343
|
#: agenda_culturel/models.py:701 agenda_culturel/views.py:366
|
||||||
msgid "Closed"
|
msgid "Closed"
|
||||||
msgstr "Fermé"
|
msgstr "Fermé"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:471
|
#: agenda_culturel/models.py:701
|
||||||
msgid "this message has been processed and no longer needs to be handled"
|
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"
|
msgstr "Ce message a été traité et ne nécessite plus d'être pris en charge"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:472
|
#: agenda_culturel/models.py:702
|
||||||
msgid "Comments"
|
msgid "Comments"
|
||||||
msgstr "Commentaires"
|
msgstr "Commentaires"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:472
|
#: agenda_culturel/models.py:702
|
||||||
msgid "Comments on the message from the moderation team"
|
msgid "Comments on the message from the moderation team"
|
||||||
msgstr "Commentaires sur ce message par l'équipe de modération"
|
msgstr "Commentaires sur ce message par l'équipe de modération"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:481
|
#: agenda_culturel/models.py:711
|
||||||
msgid "Running"
|
msgid "ical"
|
||||||
msgstr ""
|
msgstr "ical"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:482
|
#: agenda_culturel/models.py:714
|
||||||
msgid "Canceled"
|
msgid "simple"
|
||||||
msgstr "Annulé"
|
msgstr "simple"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:483
|
#: agenda_culturel/models.py:715
|
||||||
msgid "Success"
|
msgid "Headless Chromium"
|
||||||
msgstr "Succès"
|
msgstr "chromium sans interface"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:484
|
#: agenda_culturel/models.py:719
|
||||||
msgid "Failed"
|
msgid "daily"
|
||||||
msgstr "Erreur"
|
msgstr "chaque jour"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:489
|
#: agenda_culturel/models.py:720
|
||||||
|
msgid "weekly"
|
||||||
|
msgstr "chaque semaine"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:722
|
||||||
|
msgid "Processor"
|
||||||
|
msgstr "Processeur"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:723
|
||||||
|
msgid "Downloader"
|
||||||
|
msgstr "Téléchargeur"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:725
|
||||||
|
msgid "Import recurrence"
|
||||||
|
msgstr "Récurrence d'import"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:728
|
||||||
msgid "Source"
|
msgid "Source"
|
||||||
msgstr "Source"
|
msgstr "Source"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:489
|
#: agenda_culturel/models.py:728
|
||||||
msgid "URL of the source document"
|
msgid "URL of the source document"
|
||||||
msgstr "URL du document source"
|
msgstr "URL du document source"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:490
|
#: agenda_culturel/models.py:729
|
||||||
msgid "Browsable url"
|
msgid "Browsable url"
|
||||||
msgstr "URL navigable"
|
msgstr "URL navigable"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:490
|
#: agenda_culturel/models.py:729
|
||||||
msgid "URL of the corresponding document that will be shown to visitors."
|
msgid "URL of the corresponding document that will be shown to visitors."
|
||||||
msgstr "URL correspondant au document et qui sera montrée aux visiteurs"
|
msgstr "URL correspondant au document et qui sera montrée aux visiteurs"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:494
|
#: agenda_culturel/models.py:731
|
||||||
|
msgid "Address for each imported event"
|
||||||
|
msgstr "Adresse de chaque événement importé"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:733
|
||||||
|
msgid "Category of each imported event"
|
||||||
|
msgstr "Catégorie de chaque événement importé"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:734
|
||||||
|
msgid "Tags for each imported event"
|
||||||
|
msgstr "Étiquettes de chaque événement importé"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:734
|
||||||
|
msgid "A list of tags that describe each imported event."
|
||||||
|
msgstr "Une liste d'étiquettes décrivant chaque événement importé"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:746
|
||||||
|
msgid "Running"
|
||||||
|
msgstr "En cours"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:747
|
||||||
|
msgid "Canceled"
|
||||||
|
msgstr "Annulé"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:748
|
||||||
|
msgid "Success"
|
||||||
|
msgstr "Succès"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:749
|
||||||
|
msgid "Failed"
|
||||||
|
msgstr "Erreur"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:754
|
||||||
|
msgid "Recurrent import"
|
||||||
|
msgstr "Import récurrent"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:754
|
||||||
|
msgid "Reference to the recurrent import processing"
|
||||||
|
msgstr "Référence du processus d'import récurrent"
|
||||||
|
|
||||||
|
#: agenda_culturel/models.py:758
|
||||||
msgid "Error message"
|
msgid "Error message"
|
||||||
msgstr "Votre message"
|
msgstr "Votre message"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:496
|
#: agenda_culturel/models.py:760
|
||||||
msgid "Number of collected events"
|
msgid "Number of collected events"
|
||||||
msgstr "Nombre d'événements collectés"
|
msgstr "Nombre d'événements collectés"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:497
|
#: agenda_culturel/models.py:761
|
||||||
msgid "Number of imported events"
|
msgid "Number of imported events"
|
||||||
msgstr "Nombre d'événements importés"
|
msgstr "Nombre d'événements importés"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:498
|
#: agenda_culturel/models.py:762
|
||||||
msgid "Number of updated events"
|
msgid "Number of updated events"
|
||||||
msgstr "Nombre d'événements mis à jour"
|
msgstr "Nombre d'événements mis à jour"
|
||||||
|
|
||||||
#: agenda_culturel/models.py:499
|
#: agenda_culturel/models.py:763
|
||||||
msgid "Number of removed events"
|
msgid "Number of removed events"
|
||||||
msgstr "Nombre d'événements supprimés"
|
msgstr "Nombre d'événements supprimés"
|
||||||
|
|
||||||
#: agenda_culturel/settings/base.py:134
|
#: agenda_culturel/settings/base.py:135
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "anglais"
|
msgstr "anglais"
|
||||||
|
|
||||||
#: agenda_culturel/settings/base.py:135
|
#: agenda_culturel/settings/base.py:136
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr "français"
|
msgstr "français"
|
||||||
|
|
||||||
#: agenda_culturel/views.py:190
|
#: agenda_culturel/views.py:203
|
||||||
msgid "The static content has been successfully updated."
|
msgid "The static content has been successfully updated."
|
||||||
msgstr "Le contenu statique a été modifié avec succès."
|
msgstr "Le contenu statique a été modifié avec succès."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:196
|
#: agenda_culturel/views.py:209
|
||||||
msgid "The event has been successfully modified."
|
msgid "The event has been successfully modified."
|
||||||
msgstr "L'événement a été modifié avec succès."
|
msgstr "L'événement a été modifié avec succès."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:207
|
#: agenda_culturel/views.py:220
|
||||||
msgid "The event has been successfully deleted."
|
msgid "The event has been successfully deleted."
|
||||||
msgstr "L'événement a été supprimé avec succès"
|
msgstr "L'événement a été supprimé avec succès"
|
||||||
|
|
||||||
#: agenda_culturel/views.py:224
|
#: agenda_culturel/views.py:247
|
||||||
msgid "The status has been successfully modified."
|
msgid "The status has been successfully modified."
|
||||||
msgstr "Le status a été modifié avec succès."
|
msgstr "Le status a été modifié avec succès."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:246
|
#: agenda_culturel/views.py:269
|
||||||
msgid "The event is saved."
|
msgid "The event is saved."
|
||||||
msgstr "L'événement est enregistré."
|
msgstr "L'événement est enregistré."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:249
|
#: agenda_culturel/views.py:272
|
||||||
msgid ""
|
msgid ""
|
||||||
"The event has been submitted and will be published as soon as it has been "
|
"The event has been submitted and will be published as soon as it has been "
|
||||||
"validated by the moderation team."
|
"validated by the moderation team."
|
||||||
@ -358,7 +413,7 @@ msgstr ""
|
|||||||
"L'événement a été soumis et sera publié dès qu'il aura été validé par "
|
"L'événement a été soumis et sera publié dès qu'il aura été validé par "
|
||||||
"l'équipe de modération."
|
"l'équipe de modération."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:279
|
#: agenda_culturel/views.py:302
|
||||||
msgid ""
|
msgid ""
|
||||||
"The event has been successfully extracted, and you can now submit it after "
|
"The event has been successfully extracted, and you can now submit it after "
|
||||||
"modifying it if necessary."
|
"modifying it if necessary."
|
||||||
@ -366,7 +421,7 @@ msgstr ""
|
|||||||
"L'événement a été extrait avec succès, vous pouvez maintenant le soumettre "
|
"L'événement a été extrait avec succès, vous pouvez maintenant le soumettre "
|
||||||
"après l'avoir modifié au besoin."
|
"après l'avoir modifié au besoin."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:283
|
#: agenda_culturel/views.py:306
|
||||||
msgid ""
|
msgid ""
|
||||||
"Unable to extract an event from the proposed URL. Please use the form below "
|
"Unable to extract an event from the proposed URL. Please use the form below "
|
||||||
"to submit the event."
|
"to submit the event."
|
||||||
@ -374,12 +429,16 @@ msgstr ""
|
|||||||
"Impossible d'extraire un événement depuis l'URL proposée. Veuillez utiliser "
|
"Impossible d'extraire un événement depuis l'URL proposée. Veuillez utiliser "
|
||||||
"le formulaire ci-dessous pour soumettre l'événement."
|
"le formulaire ci-dessous pour soumettre l'événement."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:292
|
#: agenda_culturel/views.py:315
|
||||||
msgid "This URL has already been submitted, and you can find the event below."
|
msgid "This URL has already been submitted, and you can find the event below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Cette URL a déjà été soumise, et vous trouverez l'événement ci-dessous."
|
"Cette URL a déjà été soumise, et vous trouverez l'événement ci-dessous."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:296
|
#: agenda_culturel/views.py:319
|
||||||
|
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:321
|
||||||
msgid ""
|
msgid ""
|
||||||
"This URL has already been submitted, but has not been selected for "
|
"This URL has already been submitted, but has not been selected for "
|
||||||
"publication by the moderation team."
|
"publication by the moderation team."
|
||||||
@ -387,34 +446,87 @@ msgstr ""
|
|||||||
"Cette URL a déjà été soumise, mais n'a pas été retenue par l'équipe de "
|
"Cette URL a déjà été soumise, mais n'a pas été retenue par l'équipe de "
|
||||||
"modération pour la publication."
|
"modération pour la publication."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:298
|
#: agenda_culturel/views.py:343
|
||||||
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:320
|
|
||||||
msgid "Your message has been sent successfully."
|
msgid "Your message has been sent successfully."
|
||||||
msgstr "L'événement a été supprimé avec succès"
|
msgstr "L'événement a été supprimé avec succès"
|
||||||
|
|
||||||
#: agenda_culturel/views.py:328
|
#: agenda_culturel/views.py:351
|
||||||
msgid "The contact message properties has been successfully modified."
|
msgid "The contact message properties has been successfully modified."
|
||||||
msgstr "Les propriétés du message de contact ont été modifié avec succès."
|
msgstr "Les propriétés du message de contact ont été modifié avec succès."
|
||||||
|
|
||||||
#: agenda_culturel/views.py:343
|
#: agenda_culturel/views.py:366
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Ouvert"
|
msgstr "Ouvert"
|
||||||
|
|
||||||
#: agenda_culturel/views.py:383
|
#: agenda_culturel/views.py:406
|
||||||
msgid "Search"
|
msgid "Search"
|
||||||
msgstr "Rechercher"
|
msgstr "Rechercher"
|
||||||
|
|
||||||
#: agenda_culturel/views.py:491
|
#: agenda_culturel/views.py:521
|
||||||
msgid "The import has been run successfully."
|
msgid "The import has been run successfully."
|
||||||
msgstr "L'import a été lancé avec succès"
|
msgstr "L'import a été lancé avec succès"
|
||||||
|
|
||||||
#: agenda_culturel/views.py:521
|
#: agenda_culturel/views.py:537
|
||||||
msgid "The import has been canceled."
|
msgid "The import has been canceled."
|
||||||
msgstr "L'import a été annulé"
|
msgstr "L'import a été annulé"
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:571
|
||||||
|
msgid "The recurrent import has been successfully modified."
|
||||||
|
msgstr "L'import récurrent a été modifié avec succès."
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:577
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "The event has been successfully deleted."
|
||||||
|
msgid "The recurrent import has been successfully deleted."
|
||||||
|
msgstr "L'import récurrent a été supprimé avec succès"
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:606
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "The import has been canceled."
|
||||||
|
msgid "The import has been launched."
|
||||||
|
msgstr "L'import a été annulé"
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:665
|
||||||
|
msgid "The merge has been successfully completed."
|
||||||
|
msgstr "La fusion a été réalisée avec succès."
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:694
|
||||||
|
msgid "Events have been marked as unduplicated."
|
||||||
|
msgstr "Les événements ont été marqués comme non dupliqués."
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:711
|
||||||
|
msgid ""
|
||||||
|
"The selected event has been retained, while the other has been moved "
|
||||||
|
"to the recycle bin."
|
||||||
|
msgstr ""
|
||||||
|
"L'événement sélectionné a été conservé, l'autre a été déplacé dans la "
|
||||||
|
"corbeille."
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:713
|
||||||
|
msgid ""
|
||||||
|
"The selected event has been retained, while the others have been moved "
|
||||||
|
"to the recycle bin."
|
||||||
|
msgstr ""
|
||||||
|
"L'événement sélectionné a été conservé, les autres ont été déplacés dans la "
|
||||||
|
"corbeille."
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:719
|
||||||
|
msgid "The event has been withdrawn from the group and made independent."
|
||||||
|
msgstr "L'événement a été retiré du groupe et rendu indépendant."
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:765
|
||||||
|
msgid "The event was successfully duplicated."
|
||||||
|
msgstr "L'événement a été marqué dupliqué avec succès."
|
||||||
|
|
||||||
|
#: agenda_culturel/views.py:768
|
||||||
|
msgid ""
|
||||||
|
"The event has been successfully flagged as a duplicate. The moderation team "
|
||||||
|
"will deal with your suggestion shortly."
|
||||||
|
msgstr ""
|
||||||
|
"L'événement a été signalé comme dupliqué avec succès. Votre suggestion sera "
|
||||||
|
"prochainement prise en charge par l'équipe de modération."
|
||||||
|
|
||||||
|
|
||||||
msgid "Add another"
|
msgid "Add another"
|
||||||
msgstr "Ajouter un autre"
|
msgstr "Ajouter un autre"
|
||||||
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-01-20 15:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_better_admin_arrayfield.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agenda_culturel', '0027_set_dtstart_dtend'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RecurrentImport',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('processor', models.CharField(choices=[('ical', 'ical')], default='ical', max_length=20, verbose_name='Processor')),
|
||||||
|
('recurrence', models.CharField(choices=[('daily', 'daily'), ('weekly', 'weekly')], default='daily', max_length=10, verbose_name='Import recurrence')),
|
||||||
|
('source', models.URLField(help_text='URL of the source document', max_length=1024, 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')),
|
||||||
|
('defaultLocation', models.CharField(blank=True, help_text='Address for each imported event', max_length=512, null=True, verbose_name='Location')),
|
||||||
|
('defaultTags', django_better_admin_arrayfield.models.fields.ArrayField(base_field=models.CharField(max_length=64), blank=True, help_text='A list of tags that describe each imported event.', null=True, size=None, verbose_name='Tags for each imported event')),
|
||||||
|
('defaultCategory', models.ForeignKey(blank=True, help_text='Category of each imported event', null=True, on_delete=django.db.models.deletion.SET_NULL, to='agenda_culturel.category', verbose_name='Category')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='batchimportation',
|
||||||
|
name='recurrentImport',
|
||||||
|
field=models.ForeignKey(blank=True, editable=False, help_text='Reference to the recurrent import processing', null=True, on_delete=django.db.models.deletion.SET_NULL, to='agenda_culturel.recurrentimport', verbose_name='Recurrent import'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-01-25 21:37
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agenda_culturel', '0028_recurrentimport_batchimportation_recurrentimport'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='batchimportation',
|
||||||
|
name='browsable_url',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='batchimportation',
|
||||||
|
name='source',
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-01-26 10:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('agenda_culturel', '0029_remove_batchimportation_browsable_url_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='recurrentimport',
|
||||||
|
name='downloader',
|
||||||
|
field=models.CharField(choices=[('simple', 'simple'), ('chromium headless', 'Headless Chromium')], default='simple', max_length=20, verbose_name='Downloader'),
|
||||||
|
),
|
||||||
|
]
|
@ -503,8 +503,9 @@ class Event(models.Model):
|
|||||||
for e in events:
|
for e in events:
|
||||||
e.possibly_duplicated = group
|
e.possibly_duplicated = group
|
||||||
|
|
||||||
# finally update all events (including current)
|
# finally update all events (including current if already created)
|
||||||
Event.objects.bulk_update(events + [self], fields=["possibly_duplicated"])
|
elist = list(events) + ([self] if self.pk is not None else [])
|
||||||
|
Event.objects.bulk_update(elist, fields=["possibly_duplicated"])
|
||||||
|
|
||||||
|
|
||||||
def data_fields(all=False):
|
def data_fields(all=False):
|
||||||
@ -631,7 +632,7 @@ class Event(models.Model):
|
|||||||
nb_updated = Event.objects.bulk_update(to_update, fields = Event.data_fields() + ["imported_date", "modified_date", "uuids", "status"])
|
nb_updated = Event.objects.bulk_update(to_update, fields = Event.data_fields() + ["imported_date", "modified_date", "uuids", "status"])
|
||||||
|
|
||||||
nb_draft = 0
|
nb_draft = 0
|
||||||
if remove_missing_from_source is not None:
|
if remove_missing_from_source is not None and max_date is not None:
|
||||||
# events that are missing from the import but in database are turned into drafts
|
# events that are missing from the import but in database are turned into drafts
|
||||||
# only if they are in the future
|
# only if they are in the future
|
||||||
|
|
||||||
@ -704,6 +705,41 @@ class ContactMessage(models.Model):
|
|||||||
return ContactMessage.objects.filter(closed=False).count()
|
return ContactMessage.objects.filter(closed=False).count()
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrentImport(models.Model):
|
||||||
|
|
||||||
|
class PROCESSOR(models.TextChoices):
|
||||||
|
ICAL = "ical", _("ical")
|
||||||
|
|
||||||
|
class DOWNLOADER(models.TextChoices):
|
||||||
|
SIMPLE = "simple", _("simple")
|
||||||
|
CHROMIUMHEADLESS = "chromium headless", _("Headless Chromium")
|
||||||
|
|
||||||
|
|
||||||
|
class RECURRENCE(models.TextChoices):
|
||||||
|
DAILY = "daily", _("daily"),
|
||||||
|
WEEKLY = "weekly", _("weekly")
|
||||||
|
|
||||||
|
processor = models.CharField(_("Processor"), max_length=20, choices=PROCESSOR.choices, default=PROCESSOR.ICAL)
|
||||||
|
downloader = models.CharField(_("Downloader"), max_length=20, choices=DOWNLOADER.choices, default=DOWNLOADER.SIMPLE)
|
||||||
|
|
||||||
|
recurrence = models.CharField(_("Import recurrence"), max_length=10, choices=RECURRENCE.choices, default=RECURRENCE.DAILY)
|
||||||
|
|
||||||
|
|
||||||
|
source = models.URLField(verbose_name=_('Source'), help_text=_("URL of the source document"), max_length=1024)
|
||||||
|
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)
|
||||||
|
|
||||||
|
defaultLocation = models.CharField(verbose_name=_('Location'), help_text=_('Address for each imported event'), max_length=512, null=True, blank=True)
|
||||||
|
|
||||||
|
defaultCategory = models.ForeignKey(Category, verbose_name=_('Category'), help_text=_('Category of each imported event'), blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
|
defaultTags = ArrayField(models.CharField(max_length=64), verbose_name=_('Tags for each imported event'), help_text=_("A list of tags that describe each imported event."), blank=True, null=True)
|
||||||
|
|
||||||
|
def nb_imports(self):
|
||||||
|
return BatchImportation.objects.filter(recurrentImport=self).count()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("view_rimport", kwargs={"pk": self.pk})
|
||||||
|
|
||||||
|
|
||||||
class BatchImportation(models.Model):
|
class BatchImportation(models.Model):
|
||||||
|
|
||||||
class STATUS(models.TextChoices):
|
class STATUS(models.TextChoices):
|
||||||
@ -715,8 +751,7 @@ class BatchImportation(models.Model):
|
|||||||
|
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
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)
|
recurrentImport = models.ForeignKey(RecurrentImport, verbose_name=_('Recurrent import'), help_text=_('Reference to the recurrent import processing'), blank=True, null=True, on_delete=models.SET_NULL, editable=False)
|
||||||
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)
|
status = models.CharField(_("Status"), max_length=20, choices=STATUS.choices, default=STATUS.RUNNING)
|
||||||
|
|
||||||
|
@ -775,3 +775,9 @@ article>article {
|
|||||||
.strike {
|
.strike {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex: auto;
|
||||||
|
gap: 0.4em;
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<table role="grid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2">Identifiant</th>
|
||||||
|
<th rowspan="2">Date</th>
|
||||||
|
<th rowspan="2">Status</th>
|
||||||
|
<th rowspan="2">Action</th>
|
||||||
|
<th colspan="4">événements</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>initial</th>
|
||||||
|
<th>importés</th>
|
||||||
|
<th>mis à jour</th>
|
||||||
|
<th>dépubliés</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for obj in paginator_filter %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ obj.id }}</a></td>
|
||||||
|
<td>{{ obj.created_date }}</td>
|
||||||
|
<td><span{% if obj.status == "failed" %} data-tooltip="{{ obj.error_message }}"{% endif %}>{{ obj.status }}</span></td>
|
||||||
|
<td>{% if obj.status == "running" %}<a href="{% url 'cancel_import' obj.id %}">Annuler</a>{% endif %}</td>
|
||||||
|
<td>{% if obj.status == "success" %}{{ obj.nb_initial }}{% endif %}</td>
|
||||||
|
<td>{% if obj.status == "success" %}{{ obj.nb_imported }}{% endif %}</td>
|
||||||
|
<td>{% if obj.status == "success" %}{{ obj.nb_updated }}{% endif %}</td>
|
||||||
|
<td>{% if obj.status == "success" %}{{ obj.nb_removed }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<footer>
|
||||||
|
<span>
|
||||||
|
{% if paginator_filter.has_previous %}
|
||||||
|
<a href="?page=1" role="button">« 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 »</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</footer>
|
@ -1,12 +1,12 @@
|
|||||||
{% extends "agenda_culturel/page.html" %}
|
{% extends "agenda_culturel/page.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Importation par lot{% endblock %}
|
{% block title %}Importation manuelle{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>Importation par lot</h1>
|
<h1>Importation manuelle</h1>
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
<form method="post">{% csrf_token %}
|
<form method="post">{% csrf_token %}
|
||||||
|
@ -12,58 +12,11 @@
|
|||||||
<div class="grid two-columns">
|
<div class="grid two-columns">
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<a class="slide-buttons" href="{% url 'add_import'%}" role="button">Nouvel import</a>
|
<a class="slide-buttons" href="{% url 'add_import'%}" role="button">Import manuel</a>
|
||||||
<h1>Importations par lot</h1>
|
<h1>Importations par lot</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<table role="grid">
|
{% include "agenda_culturel/batch-imports-inc.html" with paginator_filter=paginator_filter %}
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th rowspan="2">Identifiant</th>
|
|
||||||
<th rowspan="2">Date</th>
|
|
||||||
<th rowspan="2">Status</th>
|
|
||||||
<th rowspan="2">Action</th>
|
|
||||||
<th colspan="4">événements</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>initial</th>
|
|
||||||
<th>importés</th>
|
|
||||||
<th>mis à jour</th>
|
|
||||||
<th>dépubliés</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for obj in paginator_filter %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ obj.id }}</a></td>
|
|
||||||
<td>{{ obj.created_date }}</td>
|
|
||||||
<td><span{% if obj.status == "failed" %} data-tooltip="{{ obj.error_message }}"{% endif %}>{{ obj.status }}</span></td>
|
|
||||||
<td>{% if obj.status == "running" %}<a href="{% url 'cancel_import' obj.id %}">Annuler</a>{% endif %}</td>
|
|
||||||
<td>{% if obj.status == "success" %}{{ obj.nb_initial }}{% endif %}</td>
|
|
||||||
<td>{% if obj.status == "success" %}{{ obj.nb_imported }}{% endif %}</td>
|
|
||||||
<td>{% if obj.status == "success" %}{{ obj.nb_updated }}{% endif %}</td>
|
|
||||||
<td>{% if obj.status == "success" %}{{ obj.nb_removed }}{% endif %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<footer>
|
|
||||||
<span>
|
|
||||||
{% if paginator_filter.has_previous %}
|
|
||||||
<a href="?page=1" role="button">« 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 »</a>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</footer>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{% include "agenda_culturel/side-nav.html" with current="imports" %}
|
{% include "agenda_culturel/side-nav.html" with current="imports" %}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
{% extends "agenda_culturel/page.html" %}
|
||||||
|
|
||||||
|
{% block title %}Import récurrent #{{ object.pk }}{% endblock %}
|
||||||
|
|
||||||
|
{% load cat_extra %}
|
||||||
|
{% load utils_extra %}
|
||||||
|
|
||||||
|
{% block entete_header %}
|
||||||
|
{% css_categories %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid two-columns">
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<a href="{% url 'recurrent_imports' %}" role="button">< Retour</a>
|
||||||
|
<div class="buttons slide-buttons">
|
||||||
|
<a href="{% url 'run_rimport' object.pk %}" role="button">Exécuter {% picto_from_name "download-cloud" %}</a>
|
||||||
|
<a href="{% url 'edit_rimport' object.pk %}" role="button">Éditer {% picto_from_name "edit" %}</a>
|
||||||
|
<a href="{% url 'delete_rimport' object.pk %}" role="button">Supprimer {% picto_from_name "trash" %}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Import récurrent #{{ object.pk }}</h1>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Processeur :</strong> {{ object.processor }}</li>
|
||||||
|
<li><strong>Téléchargeur :</strong> {{ object.downloader }}</li>
|
||||||
|
<li><strong>Recurrence :</strong> {{ object.recurrence }}</li>
|
||||||
|
<li><strong>Source :</strong> <a href="{{ object.source }}">{{ object.source }}</a></li>
|
||||||
|
<li><strong>Adresse naviguable :</strong> <a href="{{ object.browsable_url }}">{{ object.browsable_url }}</a></li>
|
||||||
|
<li><strong>Valeurs par défaut :</strong>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Localisation :</strong> {{ object.defaultLocation }}</li>
|
||||||
|
<li><strong>Catégorie :</strong> {{ object.defaultCategory }}</li>
|
||||||
|
<li><strong>Étiquettes :</strong>
|
||||||
|
{% for tag in object.defaultTags %}
|
||||||
|
{{ tag }}{% if not forloop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% include "agenda_culturel/batch-imports-inc.html" with paginator_filter=paginator_filter %}
|
||||||
|
|
||||||
|
</article>
|
||||||
|
|
||||||
|
|
||||||
|
{% include "agenda_culturel/side-nav.html" with current="rimports" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,18 @@
|
|||||||
|
{% extends "agenda_culturel/page.html" %}
|
||||||
|
|
||||||
|
{% block title %}Supprimer {{ object.title }}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>Suppression de l'import récurrent {{ object.pk }}</h1>
|
||||||
|
<form method="post">{% csrf_token %}
|
||||||
|
<p>Êtes-vous sûr·e de vouloir supprimer l'import récurrent « {{ object.pk }} » correspondant à la source <a href="{{ object.source }}">{{ object.source }}</a> ?</p>
|
||||||
|
{{ form }}
|
||||||
|
<div class="grid">
|
||||||
|
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
|
||||||
|
<input type="submit" value="Confirmer">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "agenda_culturel/page.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Importation récurrente{% endblock %}
|
||||||
|
|
||||||
|
{% block entete_header %}
|
||||||
|
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
|
||||||
|
<script src="/static/admin/js/jquery.init.js"></script>
|
||||||
|
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
|
||||||
|
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>Importation récurrente</h1>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<form method="post">{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<div class="grid">
|
||||||
|
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
|
||||||
|
<input type="submit" value="Envoyer">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% endblock %}
|
75
src/agenda_culturel/templates/agenda_culturel/rimports.html
Normal file
75
src/agenda_culturel/templates/agenda_culturel/rimports.html
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
{% extends "agenda_culturel/page.html" %}
|
||||||
|
|
||||||
|
{% block title %}Importations récurrentes{% 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_rimport'%}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a>
|
||||||
|
<h1>Importations récurrentes</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<table role="grid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Identifiant</th>
|
||||||
|
<th>Processeur</th>
|
||||||
|
<th>Téléchargeur</th>
|
||||||
|
<th>Récurrence</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Adresse naviguable</th>
|
||||||
|
<th>Nombre d'imports</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for obj in paginator_filter %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ obj.pk }}</a></td>
|
||||||
|
<td>{{ obj.processor }}</td>
|
||||||
|
<td>{{ obj.downloader }}</td>
|
||||||
|
<td>{{ obj.recurrence }}</td>
|
||||||
|
<td><a href="{{ obj.source }}">{{ obj.source|truncatechars_middle:32 }}</a></td>
|
||||||
|
<td><a href="{{ obj.browsable_url }}">{{ obj.browsable_url|truncatechars_middle:32 }}</a></td>
|
||||||
|
<td>{{ obj.nb_imports }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<a href="{% url 'run_rimport' obj.pk %}" role="button">Exécuter</a>
|
||||||
|
<a href="{% url 'view_rimport' obj.pk %}" role="button">Consulter</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<span>
|
||||||
|
{% if paginator_filter.has_previous %}
|
||||||
|
<a href="?page=1" role="button">« 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 »</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% include "agenda_culturel/side-nav.html" with current="rimports" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "agenda_culturel/page.html" %}
|
||||||
|
|
||||||
|
{% block title %}Lancer l'import #{{ object.pk }}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h1>Lancement de l'import #{{ object.pk }}</h1>
|
||||||
|
</header>
|
||||||
|
<form method="post">{% csrf_token %}
|
||||||
|
<p>Êtes-vous sûr·e de vouloir lancer l'import récurrent « {{ object.pk }} » correspondant à la source <a href="{{ object.source }}">{{ object.source }}</a> ?</p>
|
||||||
|
{{ form }}
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="grid">
|
||||||
|
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'recurrent_imports' %}{% endif %}" role="button" class="secondary">Annuler</a>
|
||||||
|
<input type="submit" value="Confirmer">
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -21,7 +21,17 @@
|
|||||||
<li><a {% if current == "moderation" %}class="selected" {% endif %}href="{% url 'moderation' %}">Derniers événements soumis</a>{% show_badges_events "left" %}</li>
|
<li><a {% if current == "moderation" %}class="selected" {% endif %}href="{% url 'moderation' %}">Derniers événements soumis</a>{% show_badges_events "left" %}</li>
|
||||||
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
|
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
|
||||||
<li><a {% if current == "duplicates" %}class="selected" {% endif %}href="{% url 'duplicates' %}">Gestion des doublons</a>{% show_badge_duplicated "left" %}</li>
|
<li><a {% if current == "duplicates" %}class="selected" {% endif %}href="{% url 'duplicates' %}">Gestion des doublons</a>{% show_badge_duplicated "left" %}</li>
|
||||||
<li><a {% if current == "imports" %}class="selected" {% endif %}href="{% url 'imports' %}">Importations par lot</a></li>
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<h2>Importations</h2>
|
||||||
|
</header>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a {% if current == "imports" %}class="selected" {% endif %}href="{% url 'imports' %}">Historiques des importations</a></li>
|
||||||
|
<li><a {% if current == "rimports" %}class="selected" {% endif %}href="{% url 'recurrent_imports' %}">Importations récurrentes</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</article>
|
</article>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django import template
|
from django import template
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.template.defaultfilters import stringfilter
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
@ -71,3 +72,15 @@ def picto_from_name(name, datatooltip=""):
|
|||||||
@register.filter
|
@register.filter
|
||||||
def int_to_abc(d):
|
def int_to_abc(d):
|
||||||
return auc[int(d)]
|
return auc[int(d)]
|
||||||
|
|
||||||
|
@register.filter(is_safe=True)
|
||||||
|
@stringfilter
|
||||||
|
def truncatechars_middle(value, arg):
|
||||||
|
try:
|
||||||
|
ln = int(arg)
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
if len(value) <= ln:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return '{}...{}'.format(value[:ln//2], value[-((ln+1)//2):])
|
@ -38,8 +38,14 @@ urlpatterns = [
|
|||||||
path('contactmessages', contactmessages, name='contactmessages'),
|
path('contactmessages', contactmessages, name='contactmessages'),
|
||||||
path('contactmessage/<int:pk>', ContactMessageUpdateView.as_view(), name='contactmessage'),
|
path('contactmessage/<int:pk>', ContactMessageUpdateView.as_view(), name='contactmessage'),
|
||||||
path("imports/", imports, name="imports"),
|
path("imports/", imports, name="imports"),
|
||||||
path("imports/add", BatchImportationCreateView.as_view(), name="add_import"),
|
path("imports/add", add_import, name="add_import"),
|
||||||
path("imports/<int:pk>/cancel", cancel_import, name="cancel_import"),
|
path("imports/<int:pk>/cancel", cancel_import, name="cancel_import"),
|
||||||
|
path("rimports/", recurrent_imports, name="recurrent_imports"),
|
||||||
|
path("rimports/add", RecurrentImportCreateView.as_view(), name="add_rimport"),
|
||||||
|
path("rimports/<int:pk>/view", view_rimport, name="view_rimport"),
|
||||||
|
path("rimports/<int:pk>/edit", RecurrentImportUpdateView.as_view(), name="edit_rimport"),
|
||||||
|
path("rimports/<int:pk>/delete", RecurrentImportDeleteView.as_view(), name="delete_rimport"),
|
||||||
|
path("rimports/<int:pk>/run", run_rimport, name="run_rimport"),
|
||||||
path("duplicates/", duplicates, name="duplicates"),
|
path("duplicates/", duplicates, name="duplicates"),
|
||||||
path("duplicates/<int:pk>", DuplicatedEventsDetailView.as_view(), name="view_duplicate"),
|
path("duplicates/<int:pk>", DuplicatedEventsDetailView.as_view(), name="view_duplicate"),
|
||||||
path("duplicates/<int:pk>/fix", fix_duplicate, name="fix_duplicate"),
|
path("duplicates/<int:pk>/fix", fix_duplicate, name="fix_duplicate"),
|
||||||
|
@ -11,9 +11,9 @@ from django.http import HttpResponseRedirect
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates
|
from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates, RecurrentImportForm
|
||||||
|
|
||||||
from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents
|
from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents, RecurrentImport
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
@ -32,7 +32,7 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||||||
from .calendar import CalendarMonth, CalendarWeek, CalendarDay
|
from .calendar import CalendarMonth, CalendarWeek, CalendarDay
|
||||||
from .extractors import ExtractorAllURLs
|
from .extractors import ExtractorAllURLs
|
||||||
|
|
||||||
from .celery import app as celery_app, import_events_from_json, import_events_from_url
|
from .celery import app as celery_app, import_events_from_json, run_recurrent_import
|
||||||
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import logging
|
import logging
|
||||||
@ -507,28 +507,21 @@ def imports(request):
|
|||||||
return render(request, 'agenda_culturel/imports.html', {'paginator_filter': response} )
|
return render(request, 'agenda_culturel/imports.html', {'paginator_filter': response} )
|
||||||
|
|
||||||
|
|
||||||
class BatchImportationCreateView(SuccessMessageMixin, LoginRequiredMixin, CreateView):
|
@login_required(login_url="/accounts/login/")
|
||||||
model = BatchImportation
|
def add_import(request):
|
||||||
|
form = BatchImportationForm()
|
||||||
|
|
||||||
success_url = reverse_lazy('imports')
|
if request.method == 'POST':
|
||||||
success_message = _('The import has been run successfully.')
|
form = BatchImportationForm(request.POST)
|
||||||
|
|
||||||
form_class = BatchImportationForm
|
if form.is_valid():
|
||||||
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
|
|
||||||
# run import
|
|
||||||
if "json" in form.data and form.data["json"] is not None and form.data["json"].strip() != "":
|
|
||||||
result = import_events_from_json.delay(form.data["json"])
|
result = import_events_from_json.delay(form.data["json"])
|
||||||
else:
|
|
||||||
result = import_events_from_url.delay(form.data["source"], form.data["browsable_url"])
|
|
||||||
|
|
||||||
# update the object with celery_id
|
messages.success(request, _("The import has been run successfully."))
|
||||||
form.instance.celery_id = result.id
|
return HttpResponseRedirect(reverse_lazy("imports"))
|
||||||
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
return render(request, 'agenda_culturel/batchimportation_form.html', {"form": form})
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="/accounts/login/")
|
@login_required(login_url="/accounts/login/")
|
||||||
@ -547,6 +540,78 @@ def cancel_import(request, pk):
|
|||||||
cancel_url = reverse_lazy("imports")
|
cancel_url = reverse_lazy("imports")
|
||||||
return render(request, 'agenda_culturel/cancel_import_confirm.html', {"object": import_process, "cancel_url": cancel_url})
|
return render(request, 'agenda_culturel/cancel_import_confirm.html', {"object": import_process, "cancel_url": cancel_url})
|
||||||
|
|
||||||
|
#########################
|
||||||
|
## recurrent importations
|
||||||
|
#########################
|
||||||
|
|
||||||
|
@login_required(login_url="/accounts/login/")
|
||||||
|
def recurrent_imports(request):
|
||||||
|
paginator = Paginator(RecurrentImport.objects.all().order_by("-pk"), 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/rimports.html', {'paginator_filter': response} )
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrentImportCreateView(LoginRequiredMixin, CreateView):
|
||||||
|
model = RecurrentImport
|
||||||
|
success_url = reverse_lazy('recurrent_imports')
|
||||||
|
form_class = RecurrentImportForm
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrentImportUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
||||||
|
model = RecurrentImport
|
||||||
|
form_class = RecurrentImportForm
|
||||||
|
success_message = _('The recurrent import has been successfully modified.')
|
||||||
|
|
||||||
|
|
||||||
|
class RecurrentImportDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
|
||||||
|
model = RecurrentImport
|
||||||
|
success_url = reverse_lazy('recurrent_imports')
|
||||||
|
success_message = _('The recurrent import has been successfully deleted.')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required(login_url="/accounts/login/")
|
||||||
|
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/")
|
||||||
|
def run_rimport(request, pk):
|
||||||
|
rimport = get_object_or_404(RecurrentImport, pk=pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
|
||||||
|
# run recurrent import
|
||||||
|
result = 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 })
|
||||||
|
|
||||||
|
#########################
|
||||||
|
## duplicated events
|
||||||
|
#########################
|
||||||
|
|
||||||
|
|
||||||
class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView):
|
class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView):
|
||||||
model = DuplicatedEvents
|
model = DuplicatedEvents
|
||||||
@ -597,7 +662,7 @@ def merge_duplicate(request, pk):
|
|||||||
e.status = Event.STATUS.TRASH
|
e.status = Event.STATUS.TRASH
|
||||||
Event.objects.bulk_update(events, fields=["status"])
|
Event.objects.bulk_update(events, fields=["status"])
|
||||||
|
|
||||||
messages.info(request, _("La fusion a été réalisée avec succès."))
|
messages.info(request, _("The merge has been successfully completed."))
|
||||||
return HttpResponseRedirect(new_event.get_absolute_url())
|
return HttpResponseRedirect(new_event.get_absolute_url())
|
||||||
|
|
||||||
return render(request, 'agenda_culturel/merge_duplicate.html', context={'form': form, 'object': edup})
|
return render(request, 'agenda_culturel/merge_duplicate.html', context={'form': form, 'object': edup})
|
||||||
@ -626,7 +691,7 @@ def fix_duplicate(request, pk):
|
|||||||
s_event = events[0]
|
s_event = events[0]
|
||||||
date = s_event.start_day
|
date = s_event.start_day
|
||||||
|
|
||||||
messages.success(request, _("Les événements ont été marqués comme non dupliqués."))
|
messages.success(request, _("Events have been marked as unduplicated."))
|
||||||
edup.delete()
|
edup.delete()
|
||||||
if date is None:
|
if date is None:
|
||||||
return HttpResponseRedirect(reverse_lazy("home"))
|
return HttpResponseRedirect(reverse_lazy("home"))
|
||||||
@ -643,15 +708,15 @@ def fix_duplicate(request, pk):
|
|||||||
url = selected.get_absolute_url()
|
url = selected.get_absolute_url()
|
||||||
edup.delete()
|
edup.delete()
|
||||||
if nb == 1:
|
if nb == 1:
|
||||||
messages.success(request, _("L'événement sélectionné a été conservé, l'autre a été déplacé dans la corbeille."))
|
messages.success(request, _("The selected event has been retained, while the other has been moved to the recycle bin."))
|
||||||
else:
|
else:
|
||||||
messages.success(request, _("L'événement sélectionné a été conservé, les autres ont été déplacés dans la corbeille."))
|
messages.success(request, _("The selected event has been retained, while the others have been moved to the recycle bin."))
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
elif form.is_action_remove():
|
elif form.is_action_remove():
|
||||||
event = form.get_selected_event(edup)
|
event = form.get_selected_event(edup)
|
||||||
event.possibly_duplicated = None
|
event.possibly_duplicated = None
|
||||||
event.save()
|
event.save()
|
||||||
messages.success(request, _("L'événement a été retiré du groupe et rendu indépendant."))
|
messages.success(request, _("The event has been withdrawn from the group and made independent."))
|
||||||
if edup.nb_duplicated() == 1:
|
if edup.nb_duplicated() == 1:
|
||||||
return HttpResponseRedirect(event.get_absolute_url())
|
return HttpResponseRedirect(event.get_absolute_url())
|
||||||
else:
|
else:
|
||||||
@ -697,10 +762,10 @@ def set_duplicate(request, year, month, day, pk):
|
|||||||
event.set_possibly_duplicated(selected)
|
event.set_possibly_duplicated(selected)
|
||||||
event.save()
|
event.save()
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
messages.success(request, _("L'événement a été marqué dupliqué avec succès."))
|
messages.success(request, _("The event was successfully duplicated."))
|
||||||
return HttpResponseRedirect(reverse_lazy("view_duplicate", args=[event.possibly_duplicated.pk]))
|
return HttpResponseRedirect(reverse_lazy("view_duplicate", args=[event.possibly_duplicated.pk]))
|
||||||
else:
|
else:
|
||||||
messages.info(request, _("L'événement a été signalé comme dupliqué avec succès. Votre suggestion sera prochainement prise en charge par l'équipe de modération."))
|
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 HttpResponseRedirect(event.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,3 +33,6 @@ django-compressor==4.4
|
|||||||
django-libsass==0.9
|
django-libsass==0.9
|
||||||
django-ckeditor==6.7.0
|
django-ckeditor==6.7.0
|
||||||
django-recurrence==1.11.1
|
django-recurrence==1.11.1
|
||||||
|
icalendar==5.0.11
|
||||||
|
lxml==5.1.0
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user