Commit 3cc35e53 authored by Matthieu Boileau's avatar Matthieu Boileau
Browse files

Merge branch 'dev-calendar' into 'master'

Merge calendars

See merge request !10
parents 2864c54f 1cb638b8
Pipeline #19434 passed with stages
in 18 seconds
__version__ = '0.3.5'
__version__ = '0.3.6'
......@@ -3,10 +3,15 @@ A module to export iCalendar events
"""
from icalendar import Calendar, Event, vText
import logging
from pathlib import Path
import random
import re
import uuid
import urllib.parse
log = logging.getLogger(__name__)
rd = random.Random()
rd.seed(0)
......@@ -15,6 +20,7 @@ def build_calendar(prodid: str, ical_events: list) -> str:
"""
Return a string containing the iCalendar events
"""
cal = Calendar()
cal.add('prodid', prodid)
cal.add('version', '2.0')
......@@ -46,3 +52,46 @@ def build_calendar(prodid: str, ical_events: list) -> str:
cal.add_component(event)
return cal.to_ical().decode('UTF-8')
def merge_calendars(prodid: str, sem_calendar_paths: list) -> str:
"""
Read a list of seminars iCalendar files and merge them into
a single iCalendar file
"""
cal = Calendar()
cal.add('prodid', prodid)
cal.add('version', '2.0')
for calendar_path in sorted(sem_calendar_paths):
with open(calendar_path) as f:
sem_cal = Calendar.from_ical(f.read())
m = re.match(r"-\/\/.*\/\/Séminaire (.*)\/\/.*$", sem_cal['prodid'])
# Merge only seminars
if m:
summary_prefix = m.group(1)
for event in sem_cal.walk(name='VEVENT'):
# Prepend seminar title to event summary
event['summary'] = f"{summary_prefix} - {event.get('summary')}"
cal.add_component(event)
return cal.to_ical().decode('UTF-8')
def write_calendar_file(filename: str, conf,
ical_content: str) -> str:
"""
Write calendar content to an .ics file and return encoded url
"""
ical_filepath = 'cal' / Path(filename).with_suffix('.ics')
ical_outpath = conf.output_path / ical_filepath
log.debug(f"Export ical file to {ical_outpath}")
# Create parent directory if not exists
ical_outpath.parent.mkdir(exist_ok=True, parents=True)
ical_outpath.write_text(ical_content)
if 'url' in conf.site:
ical_url = \
f"{conf.site['url']}/{ical_filepath}"
else:
ical_url = ical_outpath.as_posix()
return urllib.parse.quote(ical_url, safe='/:')
......@@ -10,7 +10,7 @@ import urllib.parse
from . import filters, mdparse
from .utils import slugify, md, parse_md_file
from .calendar import build_calendar
from .calendar import build_calendar, merge_calendars, write_calendar_file
log = logging.getLogger(__name__)
......@@ -763,18 +763,9 @@ class SeminarPage(IndexPage):
-//{self.conf.site['calendar']['organization']}//{self.seminar['type']} "
f"{self.title}//FR")
ical_content = build_calendar(prodid, ical_events)
ical_filepath = 'cal' / Path(self._get_filename()).with_suffix('.ics')
ical_outpath = self.conf.output_path / ical_filepath
log.debug(f"Export ical file to {ical_outpath}")
# Create parent directory if not exists
ical_outpath.parent.mkdir(exist_ok=True, parents=True)
ical_outpath.write_text(ical_content)
if 'url' in self.conf.site:
self.ical_url = \
f"{self.conf.site['url']}/{ical_filepath}"
else:
self.ical_url = ical_outpath.as_posix()
self.ical_url = urllib.parse.quote(self.ical_url, safe='/:')
self.ical_url = write_calendar_file(self._get_filename(),
self.conf,
ical_content)
def get_index_node(self) -> list:
"""Overload get_index_node() for seminar pages"""
......@@ -825,6 +816,21 @@ class SeminarTopIndexPage(Page):
"""html_path corresponds to dropdown_url"""
return "active" if dropdown_url == "séminaires-actifs.html" else ""
def _export_ical(self):
prodid = (f"\
-//{self.conf.site['calendar']['organization']}//séminaires//FR")
ics_file_paths = Path(self.conf.output_path / 'cal').glob('**/*.ics')
ical_content = merge_calendars(prodid, ics_file_paths)
self.ical_url = write_calendar_file('séminaires.ics',
self.conf,
ical_content)
def render(self):
if 'calendar' in self.conf.site and self.status == 'actif':
self._export_ical()
super().render()
class Paginator:
"""
......
......@@ -113,3 +113,18 @@
</div>
{% endif %}
{% endmacro %}
{% macro ical_action(title) %}
{% if page.ical_url %}
{% call fixed_action(icon="event") %}
<div class="center">
<h3>{{ title }}</h3>
Ajouter cette URL aux abonnements de votre calendrier électronique :<br>
<code><span id="content_to_copy">{{ page.ical_url }}</span></code>
<button onclick="CopyToClipBoard('Lien copié !')" class="waves-effect waves-teal btn-flat">
<i class="material-icons">content_copy</i>
</button>
</div>
{% endcall %}
{% endif %}
{% endmacro %}
{% extends "base.html" %}
{% block fixed_action %}
{% if page.ical_url %}
{% call macros.fixed_action(icon="event") %}
<div class="center">
<h3>S'abonner au séminaire</h3>
Ajouter cette URL aux abonnements de votre calendrier électronique :<br>
<code><span id="content_to_copy">{{ page.ical_url }}</span></code>
<button onclick="CopyToClipBoard('Lien copié !')" class="waves-effect waves-teal btn-flat">
<i class="material-icons">content_copy</i>
</button>
</div>
{% endcall %}
{% endif %}
{{ macros.ical_action(title="S'abonner au séminaire") }}
{% endblock fixed_action %}
{% block content %}
......
{% extends "base.html" %}
{% block fixed_action %}
{{ macros.ical_action(title="S'abonner aux séminaires") }}
{% endblock fixed_action %}
{% block content %}
<div class="section center title">
......
from faker import Faker
from pathlib import Path
import filecmp
from sgelt import calendar
def test_calendar():
def create_ical_events(n: int) -> list:
fake = Faker(['fr_FR'])
Faker.seed(4321)
ical_events = []
for _ in range(5):
for _ in range(n):
dstart = fake.date_time()
dend = dstart + fake.time_delta()
ical_event = {
......@@ -24,6 +26,32 @@ def test_calendar():
if fake.boolean(chance_of_getting_true=50):
ical_event['description'] = fake.paragraph(10)
ical_events.append(ical_event)
return ical_events
def create_calendar(tmpdir, sem_name=None):
fake = Faker(['fr_FR'])
Faker.seed(4321)
def write_file(filename: str, ical_content: str):
ical_outpath = tmpdir / Path(filename).with_suffix('.ics')
ical_outpath.write_text(ical_content, encoding='utf-8')
return ical_outpath
if sem_name is None:
sem_name = fake.name()
ical_events = create_ical_events(fake.random_int(min=2, max=5))
ical_content = calendar.build_calendar(
prodid=f'-//Pilab//Séminaire {sem_name}//FR',
ical_events=ical_events)
write_file(sem_name, ical_content)
return ical_content
def test_build_calendar():
ical_events = create_ical_events(5)
s = calendar.build_calendar(prodid="-//Example Corp.//CalDAV Client//EN",
ical_events=ical_events)
......@@ -107,3 +135,79 @@ URL:http%3A//www.robin.com/
END:VEVENT
END:VCALENDAR
""".replace('\n', '\r\n')
def test_merge_calendars(tmpdir):
for _ in range(3):
create_calendar(tmpdir)
ics_file_paths = Path(tmpdir).glob('**/*.ics')
prodid = f'-//Pilab//Séminaires//FR'
ical_merged = calendar.merge_calendars(prodid, ics_file_paths)
assert ical_merged.startswith("""\
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Pilab//Séminaires//FR
BEGIN:VEVENT
SUMMARY:Antoine-Lucas Andre - Retourner trois passer calme pleurer si.
DTSTART;VALUE=DATE-TIME:19870422T043021
DTEND;VALUE=DATE-TIME:19870422T043021
DTSTAMP;VALUE=DATE-TIME:19870422T043021Z
""".replace('\n', '\r\n'))
assert ical_merged.endswith("""\
DESCRIPTION:Ah simple tourner dos parent. Aujourd'Hui prêter user. Choix
sentiment durer reconnaître certain or. Attention soir pleurer droite eh
colline beauté. Humide contre dieu. Accorder ici abri toi éclairer muet
départ. Entre verre clair éprouver homme. Projet longtemps malgré voie
quelque fumée. Lors maison choisir grâce appartement pied coup continuer
. Inquiéter ou même prouver.
LOCATION:Me retenir
PRIORITY:5
URL:http://www.raymond.fr/
END:VEVENT
END:VCALENDAR
""".replace('\n', '\r\n'))
def test_write_calendar_file(tmpdir, miniwebsite):
sem_name = 'seminaire_test'
ical_content = create_calendar(tmpdir, sem_name=sem_name)
ical_url = calendar.write_calendar_file(sem_name, miniwebsite.conf,
ical_content)
assert ical_url == f"{miniwebsite.conf.site['url']}/cal/{sem_name}.ics"
assert filecmp.cmp(
f"{tmpdir}/{sem_name}.ics",
miniwebsite.conf.output_path / "cal" / f"{sem_name}.ics")
def test_SeminarTopIndexPage(miniwebsite):
miniwebsite.build()
ical_filepath = Path('cal/séminaires.ics')
ical_outpath = miniwebsite.conf.output_path / ical_filepath
ical_content = ical_outpath.read_text()
assert ical_content.startswith("""\
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Pilab//séminaires//FR
BEGIN:VEVENT
SUMMARY:Beau souffrance réveiller beauté horizon manquer - Admettre prop
re existence crainte mot prochain bas
DTSTART;VALUE=DATE-TIME:20180625T042207
DTEND;VALUE=DATE-TIME:20180625T052207
DTSTAMP;VALUE=DATE-TIME:20180625T042207Z
""")
assert ical_content.endswith("""\
DESCRIPTION:Saint extraordinaire pur couler exiger vif retour anglais. Noi
r autrement papa rapidement. Raison près arbre en révéler phrase. Air d
éjà droit verser offrir. Aucun condition exiger nouveau grand disparaît
re terrain eau. Brûler étendre quarante pain. Bon fait blanc rire sortir
douze. L'Une terreur religion contenir. Direction depuis respecter créer
. Courir et durant. Abri huit reprendre reculer chiffre.
LOCATION:salle de troubler
PRIORITY:5
URL:https://fakelab.fk/s%C3%A9minaire/s%C3%A9minaire-donc-repas-%C3%A9tern
el-sein-travers-p%C3%A9n%C3%A9trer.html
END:VEVENT
END:VCALENDAR
""")
......@@ -315,8 +315,10 @@ def test_SeminarPage(miniwebsite):
ical_outpath = page.conf.output_path / ical_filepath
# Read the 20 first lines of the ical file
with open(ical_outpath) as f:
head = ''.join((next(f) for _ in range(20)))
assert head == """\
lines = f.readlines()
truncated_head = ''.join(line for line in lines[:20]
if not line.startswith('UID'))
assert truncated_head == """\
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Pilab//Séminaire Beau souffrance réveiller beauté horizon man
......@@ -326,7 +328,6 @@ SUMMARY:Admettre propre existence crainte mot prochain bas
DTSTART;VALUE=DATE-TIME:20180625T042207
DTEND;VALUE=DATE-TIME:20180625T052207
DTSTAMP;VALUE=DATE-TIME:20180625T042207Z
UID:e443df78-9558-867f-5ba9-1faf7a024204
DESCRIPTION:Selon empêcher problème nuage. Paysan époque voici avenir f
leur déjà. Compte avenir divers verre. Se avant plutôt mine demander. S
érieux que prison. Profond doucement descendre prouver type. Expérience
......@@ -359,6 +360,8 @@ def test_SeminarTopIndexPage(miniwebsite):
'text': 'Séminaires et groupes de travail',
'title': 'Séminaires et groupes de travail',
'url': 'séminaires-actifs.html'}
assert page.ical_url == "https://fakelab.fk/cal/s%C3%A9minaires.ics"
# The rest of SeminarTopIndexPage calendar test is done in test_calendar.py
def test_IndexPage(miniwebsite):
......@@ -408,11 +411,11 @@ date: 2019-06-23
category: news
---
## Prière enfance protéger lueur étoile paysage doucement
# Prière enfance protéger lueur étoile paysage doucement
Miser rapporter faire durer aventure. Coeur morceau enfin portier fermer quarante.
## Français troisième retrouver ailleurs céder passer
# Français troisième retrouver ailleurs céder passer
Foi clair expliquer. Importance remonter compte croire mal. Heureux beau distance sérieux précipiter tracer.
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment