sgelt 4.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sgelt/__init__.py +11 -0
- sgelt/__main__.py +6 -0
- sgelt/__version__.py +1 -0
- sgelt/calendar.py +105 -0
- sgelt/config.py +179 -0
- sgelt/data.py +42 -0
- sgelt/filters.py +282 -0
- sgelt/main.py +94 -0
- sgelt/mdextend.py +16 -0
- sgelt/mdparse.py +138 -0
- sgelt/pages.py +973 -0
- sgelt/py.typed +0 -0
- sgelt/serve.py +81 -0
- sgelt/sgelt.py +330 -0
- sgelt/theme/LICENSE +21 -0
- sgelt/theme/css/materialize.css +9058 -0
- sgelt/theme/css/materialize.min.css +13 -0
- sgelt/theme/css/style.css +695 -0
- sgelt/theme/img/favicon.ico +0 -0
- sgelt/theme/img/iss.jpg +0 -0
- sgelt/theme/img/logo_jinja.png +0 -0
- sgelt/theme/img/logo_materialize.png +0 -0
- sgelt/theme/img/logo_pi.png +0 -0
- sgelt/theme/img/logo_python.png +0 -0
- sgelt/theme/js/delaunay.js +65 -0
- sgelt/theme/js/init.js +30 -0
- sgelt/theme/js/lunr-search-result.js +106 -0
- sgelt/theme/js/lunr.js +3573 -0
- sgelt/theme/js/materialize.js +12337 -0
- sgelt/theme/js/materialize.min.js +6 -0
- sgelt/theme/js/parallax.js +412 -0
- sgelt/theme/js/parallax.min.js +6 -0
- sgelt/theme/templates/actualites-bib.html +13 -0
- sgelt/theme/templates/actualites.html +39 -0
- sgelt/theme/templates/agenda.html +25 -0
- sgelt/theme/templates/article.html +44 -0
- sgelt/theme/templates/base.html +126 -0
- sgelt/theme/templates/conference.html +119 -0
- sgelt/theme/templates/equipe.html +14 -0
- sgelt/theme/templates/index.html +85 -0
- sgelt/theme/templates/macros.jinja.html +182 -0
- sgelt/theme/templates/page.html +24 -0
- sgelt/theme/templates/partial/date_place.html +17 -0
- sgelt/theme/templates/partial/footer.html +54 -0
- sgelt/theme/templates/partial/nav.html +46 -0
- sgelt/theme/templates/partial/news_button.html +16 -0
- sgelt/theme/templates/search.html +45 -0
- sgelt/theme/templates/seminaire.html +56 -0
- sgelt/theme/templates/seminaires.html +56 -0
- sgelt/theme/templates/soutenance.html +58 -0
- sgelt/utils.py +208 -0
- sgelt/watch.py +81 -0
- sgelt-4.8.1.dist-info/LICENSE +21 -0
- sgelt-4.8.1.dist-info/METADATA +117 -0
- sgelt-4.8.1.dist-info/RECORD +58 -0
- sgelt-4.8.1.dist-info/WHEEL +5 -0
- sgelt-4.8.1.dist-info/entry_points.txt +2 -0
- sgelt-4.8.1.dist-info/top_level.txt +1 -0
sgelt/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Set logging, locale and package_dir"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import locale
|
|
5
|
+
from .__version__ import __version__
|
|
6
|
+
|
|
7
|
+
__all__ = ['__version__', 'package_dir']
|
|
8
|
+
|
|
9
|
+
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
|
|
10
|
+
|
|
11
|
+
package_dir = Path(__file__).parent
|
sgelt/__main__.py
ADDED
sgelt/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '4.8.1'
|
sgelt/calendar.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module to export iCalendar events
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from icalendar import Calendar, Event, vText
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import pytz
|
|
9
|
+
import random
|
|
10
|
+
import re
|
|
11
|
+
import uuid
|
|
12
|
+
import urllib.parse
|
|
13
|
+
|
|
14
|
+
from sgelt.utils import slugify
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
rd = random.Random()
|
|
19
|
+
rd.seed(0)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_calendar(prodid: str, ical_events: list) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Return a string containing the iCalendar events
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
cal = Calendar()
|
|
28
|
+
cal.add('prodid', prodid)
|
|
29
|
+
cal.add('version', '2.0')
|
|
30
|
+
|
|
31
|
+
tz = pytz.timezone('Europe/Paris')
|
|
32
|
+
|
|
33
|
+
for ical_event in ical_events:
|
|
34
|
+
event = Event()
|
|
35
|
+
event.add('summary', ical_event['summary'])
|
|
36
|
+
event.add('dtstart', tz.localize(ical_event['dtstart']).astimezone(tz))
|
|
37
|
+
event.add('dtend', tz.localize(ical_event['dtend']).astimezone(tz))
|
|
38
|
+
event.add('dtstamp', ical_event['dtstamp'])
|
|
39
|
+
try:
|
|
40
|
+
event['uid'] = ical_event['uuid']
|
|
41
|
+
except KeyError:
|
|
42
|
+
event['uid'] = str(uuid.UUID(int=rd.getrandbits(128)))
|
|
43
|
+
try:
|
|
44
|
+
event['location'] = vText(ical_event['location'])
|
|
45
|
+
except KeyError:
|
|
46
|
+
pass
|
|
47
|
+
try:
|
|
48
|
+
event['description'] = vText(ical_event['description'])
|
|
49
|
+
except KeyError:
|
|
50
|
+
pass
|
|
51
|
+
try:
|
|
52
|
+
# Encode URL for Apple Calendar
|
|
53
|
+
event['url'] = vText(urllib.parse.quote(ical_event['url'],
|
|
54
|
+
safe='/:'))
|
|
55
|
+
except KeyError:
|
|
56
|
+
pass
|
|
57
|
+
event.add('priority', 5)
|
|
58
|
+
cal.add_component(event)
|
|
59
|
+
|
|
60
|
+
return cal.to_ical().decode('UTF-8')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def merge_calendars(prodid: str, sem_calendar_paths: list,
|
|
64
|
+
event_types: set) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Read a list of seminars iCalendar files and merge them into
|
|
67
|
+
a single iCalendar file
|
|
68
|
+
"""
|
|
69
|
+
cal = Calendar()
|
|
70
|
+
cal.add('prodid', prodid)
|
|
71
|
+
cal.add('version', '2.0')
|
|
72
|
+
|
|
73
|
+
for calendar_path in sorted(sem_calendar_paths):
|
|
74
|
+
with open(calendar_path) as f:
|
|
75
|
+
sem_cal = Calendar.from_ical(f.read())
|
|
76
|
+
# Merge only seminars and Colloquium
|
|
77
|
+
pattern = r"^-\/\/.*\/\/((" + "|".join(event_types) + r") .*)\/\/.*$"
|
|
78
|
+
m = re.match(pattern, sem_cal['prodid'])
|
|
79
|
+
if m:
|
|
80
|
+
summary_prefix = m.group(1)
|
|
81
|
+
for event in sem_cal.walk(name='VEVENT'):
|
|
82
|
+
# Prepend seminar title to event summary
|
|
83
|
+
event['summary'] = f"{summary_prefix} - {event.get('summary')}"
|
|
84
|
+
cal.add_component(event)
|
|
85
|
+
|
|
86
|
+
return cal.to_ical().decode('UTF-8')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def write_calendar_file(filename: str, conf,
|
|
90
|
+
ical_content: str) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Write calendar content to an .ics file and return encoded url
|
|
93
|
+
"""
|
|
94
|
+
ical_filepath = 'cal' / Path(slugify(filename)).with_suffix('.ics')
|
|
95
|
+
ical_outpath = conf.output_path / ical_filepath
|
|
96
|
+
log.debug(f"Export ical file to {ical_outpath}")
|
|
97
|
+
# Create parent directory if not exists
|
|
98
|
+
ical_outpath.parent.mkdir(exist_ok=True, parents=True)
|
|
99
|
+
ical_outpath.write_text(ical_content)
|
|
100
|
+
if 'url' in conf.site:
|
|
101
|
+
ical_url = \
|
|
102
|
+
f"{conf.site['url']}/{ical_filepath}"
|
|
103
|
+
else:
|
|
104
|
+
ical_url = ical_outpath.as_posix()
|
|
105
|
+
return urllib.parse.quote(ical_url, safe='/:')
|
sgelt/config.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Define site configuration class and instantiate in a conf object"""
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
import jinja2
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from . import filters, package_dir, __version__
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Config:
|
|
15
|
+
"""A class to store site configuration"""
|
|
16
|
+
|
|
17
|
+
# Default config dict that may be overriden by user values
|
|
18
|
+
def_conf = dict(
|
|
19
|
+
default_theme_path=package_dir / 'theme',
|
|
20
|
+
theme_path=None,
|
|
21
|
+
output_path='output',
|
|
22
|
+
content_path='content',
|
|
23
|
+
config_path='config.yml',
|
|
24
|
+
static_payload=('css', 'js', 'img'),
|
|
25
|
+
json_agenda_path='agenda_mysql.json',
|
|
26
|
+
json_teams_path='teams.json',
|
|
27
|
+
search_index=True
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __init__(self, args=None, config_path=None):
|
|
31
|
+
"""Set class attributes from default"""
|
|
32
|
+
self.args = args
|
|
33
|
+
conf = self.def_conf.copy()
|
|
34
|
+
self._set_attributes(conf)
|
|
35
|
+
# Update config.yml default path
|
|
36
|
+
if config_path is not None:
|
|
37
|
+
self.update(config_path=config_path)
|
|
38
|
+
else:
|
|
39
|
+
try:
|
|
40
|
+
self.update(config_path=self.args.config_path)
|
|
41
|
+
except AttributeError:
|
|
42
|
+
pass
|
|
43
|
+
self.site = None
|
|
44
|
+
|
|
45
|
+
def save_args(self) -> dict:
|
|
46
|
+
"""Return a dict of config.__init__() arguments"""
|
|
47
|
+
return {'args': self.args, 'config_path': self.config_path}
|
|
48
|
+
|
|
49
|
+
def update(self, **kwargs):
|
|
50
|
+
"""Update object attributes from names args"""
|
|
51
|
+
conf = vars(self) # get attributes as dict
|
|
52
|
+
# Update conf dict with init args if key exists
|
|
53
|
+
for k, v in kwargs.items():
|
|
54
|
+
if k in self.def_conf:
|
|
55
|
+
conf[k] = v
|
|
56
|
+
else:
|
|
57
|
+
log.warning(f"Cannot update conf dict with unknown key: {k}")
|
|
58
|
+
self._set_attributes(conf)
|
|
59
|
+
|
|
60
|
+
def load(self):
|
|
61
|
+
"""Parse YAML config file and override with CLI args"""
|
|
62
|
+
|
|
63
|
+
# Read config.yml file
|
|
64
|
+
try:
|
|
65
|
+
with open(self.config_path) as f:
|
|
66
|
+
file_conf = yaml.safe_load(f)
|
|
67
|
+
except FileNotFoundError as e:
|
|
68
|
+
log.error(e)
|
|
69
|
+
log.debug(self)
|
|
70
|
+
import sys
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
# site dict becomes .site attribute
|
|
74
|
+
self.site = file_conf.pop('site')
|
|
75
|
+
|
|
76
|
+
# Convert path/to/file.md -> path/to/file.html
|
|
77
|
+
for dropdown_items in self.site['nav_items'].values():
|
|
78
|
+
try:
|
|
79
|
+
for dropdown_name, filename in dropdown_items.items():
|
|
80
|
+
filepath = Path(filename)
|
|
81
|
+
# Convert markdown paths to html paths
|
|
82
|
+
if filepath.suffix == '.md':
|
|
83
|
+
dropdown_items[dropdown_name] = filepath.with_suffix(
|
|
84
|
+
'.html').as_posix()
|
|
85
|
+
except AttributeError:
|
|
86
|
+
# dropdown_items is not an actual dropdown dict
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
if 'today' in self.site:
|
|
90
|
+
# Convert today as date if exists
|
|
91
|
+
if type(self.site['today']) is not date:
|
|
92
|
+
self.site['today'] = self.site['today'].date()
|
|
93
|
+
else:
|
|
94
|
+
# Use today date
|
|
95
|
+
self.site['today'] = date.today()
|
|
96
|
+
|
|
97
|
+
# update other attributes (same as in def_conf)
|
|
98
|
+
self.update(**file_conf)
|
|
99
|
+
|
|
100
|
+
# Override with CLI args
|
|
101
|
+
args = self.args
|
|
102
|
+
if args is not None:
|
|
103
|
+
if args.json_agenda:
|
|
104
|
+
self.update(json_agenda_path=args.json_agenda)
|
|
105
|
+
if args.json_teams:
|
|
106
|
+
self.update(json_teams_path=args.json_teams)
|
|
107
|
+
if args.content:
|
|
108
|
+
self.update(content_path=args.content)
|
|
109
|
+
if args.output:
|
|
110
|
+
self.update(output_path=args.output)
|
|
111
|
+
if not args.search_index:
|
|
112
|
+
self.update(search_index=False)
|
|
113
|
+
|
|
114
|
+
def _set_attributes(self, conf: dict):
|
|
115
|
+
"""From conf dict and default values, set class attributes"""
|
|
116
|
+
for k, v in conf.items():
|
|
117
|
+
if k != 'env':
|
|
118
|
+
if isinstance(v, list) or isinstance(v, tuple):
|
|
119
|
+
setattr(self, k, [Path(item) for item in v])
|
|
120
|
+
elif isinstance(v, dict) or isinstance(v, type(None)):
|
|
121
|
+
setattr(self, k, v)
|
|
122
|
+
elif isinstance(v, type(None)):
|
|
123
|
+
pass
|
|
124
|
+
else:
|
|
125
|
+
try:
|
|
126
|
+
setattr(self, k, Path(v))
|
|
127
|
+
except TypeError:
|
|
128
|
+
setattr(self, k, v)
|
|
129
|
+
|
|
130
|
+
self.templates_paths = [self.default_theme_path / Path('templates')]
|
|
131
|
+
try:
|
|
132
|
+
if conf['theme_path'] and conf['theme_path'].is_relative_to(
|
|
133
|
+
self.config_path.parent):
|
|
134
|
+
conf_theme_path = conf['theme_path']
|
|
135
|
+
else:
|
|
136
|
+
conf_theme_path = self.config_path.parent / conf['theme_path']
|
|
137
|
+
user_template_path = conf_theme_path / 'templates'
|
|
138
|
+
if user_template_path.exists(): # Add override template dir
|
|
139
|
+
self.templates_paths.append(user_template_path)
|
|
140
|
+
except TypeError: # self.theme_path is None thus skip
|
|
141
|
+
pass
|
|
142
|
+
self.env = jinja2.Environment(
|
|
143
|
+
loader=jinja2.FileSystemLoader(self.templates_paths)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Add filters
|
|
147
|
+
self.env.filters['homepage'] = filters.homepage
|
|
148
|
+
self.env.filters['agenda'] = filters.agenda
|
|
149
|
+
self.env.filters['event_date_time'] = filters.event_date_time
|
|
150
|
+
self.env.filters['categ_to_target'] = filters.categ_to_target
|
|
151
|
+
self.env.filters['url'] = filters.url
|
|
152
|
+
self.env.filters['external_url'] = filters.external_url
|
|
153
|
+
self.env.filters['event_selector'] = filters.event_selector
|
|
154
|
+
self.env.filters['sort_by_date'] = filters.sort_by_date
|
|
155
|
+
self.env.filters['homepage_news'] = filters.homepage_news
|
|
156
|
+
self.env.filters['news_page'] = filters.news_page
|
|
157
|
+
self.env.filters['dropdown_items'] = filters.dropdown_items
|
|
158
|
+
self.env.filters['real_teams'] = filters.real_teams
|
|
159
|
+
self.env.filters['teams'] = filters.teams
|
|
160
|
+
self.env.filters['organizing_teams'] = filters.organizing_teams
|
|
161
|
+
|
|
162
|
+
# Global variables
|
|
163
|
+
self.env.globals['datetime_min'] = datetime.min
|
|
164
|
+
self.env.globals['sgelt_version'] = __version__
|
|
165
|
+
|
|
166
|
+
def __repr__(self):
|
|
167
|
+
def _format(k, v):
|
|
168
|
+
if isinstance(v, list):
|
|
169
|
+
return "{}:\n{}".format(k, '\n'.join((f" - {item}"
|
|
170
|
+
for item in v)))
|
|
171
|
+
elif isinstance(v, dict):
|
|
172
|
+
import textwrap
|
|
173
|
+
return f"""\
|
|
174
|
+
{k}:\n{textwrap.indent(yaml.dump(v, allow_unicode=True), 2 * ' ')}"""
|
|
175
|
+
else:
|
|
176
|
+
return f"{k}: {v}"
|
|
177
|
+
return '\n'.join((_format(k, v)
|
|
178
|
+
for k, v in vars(self).items()
|
|
179
|
+
if not isinstance(v, jinja2.Environment)))
|
sgelt/data.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module to store agenda, news, events and teams data
|
|
3
|
+
To be populated and modified by sgelt module
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .utils import read_data
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Data:
|
|
10
|
+
"""A class to store site data"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, conf):
|
|
13
|
+
"""Initialize with configuration object and empty data"""
|
|
14
|
+
|
|
15
|
+
self.conf = conf # a configuration object
|
|
16
|
+
# read from agenda.json
|
|
17
|
+
self.conferences = []
|
|
18
|
+
self.defenses = []
|
|
19
|
+
self.seminars = []
|
|
20
|
+
|
|
21
|
+
self.teams = {} # read from teams.json
|
|
22
|
+
|
|
23
|
+
self.news = [] # will be populated by registering news
|
|
24
|
+
self.libnews = [] # will be populated by registering libnews
|
|
25
|
+
self.events = [] # will be populated by registering events
|
|
26
|
+
|
|
27
|
+
def read_files(self):
|
|
28
|
+
"""Read data from json files"""
|
|
29
|
+
|
|
30
|
+
# Read agenda data from json file
|
|
31
|
+
agenda = read_data(self.conf.json_agenda_path)
|
|
32
|
+
self.conferences = agenda['conferences']
|
|
33
|
+
self.defenses = agenda['defenses']
|
|
34
|
+
self.seminars = agenda['seminars']
|
|
35
|
+
|
|
36
|
+
# Read team data from json file
|
|
37
|
+
self.teams = read_data(self.conf.json_teams_path)
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
from pprint import pformat
|
|
41
|
+
return '\n'.join((f"{k}: {pformat(v)}"
|
|
42
|
+
for k, v in vars(self).items()))
|
sgelt/filters.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import types
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
import jinja2
|
|
4
|
+
|
|
5
|
+
from .utils import slugify
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def exclude_events(events):
|
|
9
|
+
"""Exclude events whose speaker is TBA"""
|
|
10
|
+
for event in events:
|
|
11
|
+
try:
|
|
12
|
+
speaker = event['speaker']
|
|
13
|
+
if 'tba' not in speaker.lower().split():
|
|
14
|
+
yield event
|
|
15
|
+
except KeyError:
|
|
16
|
+
yield event
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def filter_titles(events):
|
|
20
|
+
"""If event title is in to_filter, modify it to 'À venir'"""
|
|
21
|
+
title_filter = 'tba', 'à venir', 'à confirmer'
|
|
22
|
+
for event in events:
|
|
23
|
+
if event['title'].lower() in title_filter:
|
|
24
|
+
event['title'] = 'À venir'
|
|
25
|
+
yield event
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def sort_by_date(events, reverse=False) -> list:
|
|
29
|
+
"""Revert events sort by date"""
|
|
30
|
+
event_list = list(events) if isinstance(
|
|
31
|
+
events, types.GeneratorType) else events
|
|
32
|
+
return sorted(event_list, key=lambda e: e['date'], reverse=reverse)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@jinja2.pass_context
|
|
36
|
+
def homepage(context, events) -> list:
|
|
37
|
+
"""
|
|
38
|
+
- exclude TBA authors
|
|
39
|
+
- filter TBA titles
|
|
40
|
+
- (reverse) sort by date
|
|
41
|
+
- keep only 6 first events
|
|
42
|
+
"""
|
|
43
|
+
site = context['site']
|
|
44
|
+
selected_events = exclude_events(events)
|
|
45
|
+
filtered_title_events = filter_titles(selected_events)
|
|
46
|
+
sorted_events = sort_by_date(filtered_title_events)
|
|
47
|
+
next_events = [e for e in sorted_events
|
|
48
|
+
if e.get('end', e['date']).date() >= site['today']]
|
|
49
|
+
return next_events[:6]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@jinja2.pass_context
|
|
53
|
+
def homepage_news(context, site_news):
|
|
54
|
+
"""
|
|
55
|
+
- keep only recent news
|
|
56
|
+
- (reverse) sort by date
|
|
57
|
+
"""
|
|
58
|
+
site = context['site']
|
|
59
|
+
selected_news = (news for news in site_news if recent_news(news, site))
|
|
60
|
+
return sort_by_date(selected_news, reverse=True)[:5]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@jinja2.pass_context
|
|
64
|
+
def agenda(context, events):
|
|
65
|
+
"""
|
|
66
|
+
- keep same category
|
|
67
|
+
- keep same year
|
|
68
|
+
- exclude TBA authors
|
|
69
|
+
- filter TBA titles
|
|
70
|
+
- (reverse) sort by date
|
|
71
|
+
"""
|
|
72
|
+
page = context['page']
|
|
73
|
+
site = context['site']
|
|
74
|
+
if page.category:
|
|
75
|
+
category_selection = (
|
|
76
|
+
event for event in events if event['category'] == page.category)
|
|
77
|
+
else:
|
|
78
|
+
category_selection = (event for event in events if event['category'])
|
|
79
|
+
|
|
80
|
+
if page.name:
|
|
81
|
+
name_selection = (
|
|
82
|
+
event for event in category_selection
|
|
83
|
+
if page_name(event, site) == page.name
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
name_selection = category_selection
|
|
87
|
+
|
|
88
|
+
author_selection = exclude_events(name_selection)
|
|
89
|
+
filter_titles_events = filter_titles(author_selection)
|
|
90
|
+
sorted_events = sort_by_date(filter_titles_events)
|
|
91
|
+
return sorted_events
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def event_date_time(event, weekday=False, sep=':') -> str:
|
|
95
|
+
"""Return event date(s) (and time)"""
|
|
96
|
+
|
|
97
|
+
start = event['date']
|
|
98
|
+
try:
|
|
99
|
+
end = event['end']
|
|
100
|
+
if end != start:
|
|
101
|
+
if end.month == start.month:
|
|
102
|
+
return f"Du {start.day} au {end.day} {end.strftime('%B %Y')}"
|
|
103
|
+
else:
|
|
104
|
+
return (f"Du {start.day} {start.strftime('%B')} au {end.day} "
|
|
105
|
+
f"{end.strftime('%B %Y')}")
|
|
106
|
+
else:
|
|
107
|
+
return f"""{start.strftime(
|
|
108
|
+
f"{'%A ' if weekday else ''}%-d %B %Y").capitalize()}"""
|
|
109
|
+
|
|
110
|
+
except KeyError:
|
|
111
|
+
date_time = start.strftime(
|
|
112
|
+
f"{'%A ' if weekday else ''}%-d %B %Y").capitalize()
|
|
113
|
+
if start.strftime("%H:%M") != '00:00':
|
|
114
|
+
format = f"%H{sep}%M"
|
|
115
|
+
date_time += f" - {start.strftime(format)}"
|
|
116
|
+
return date_time
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def categ_to_target(event):
|
|
120
|
+
"""Return the target corresponding to event category"""
|
|
121
|
+
category = event['category']
|
|
122
|
+
if category == 'groupe de travail':
|
|
123
|
+
return "groupes-de-travail"
|
|
124
|
+
else:
|
|
125
|
+
return f"{slugify(category)}s"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@jinja2.pass_context
|
|
129
|
+
def url(context: dict, value: str) -> str:
|
|
130
|
+
"""Return the relative url"""
|
|
131
|
+
if value.startswith(('http://', 'https://', '#')):
|
|
132
|
+
return value
|
|
133
|
+
else:
|
|
134
|
+
return context['page'].path_depth * '../' + value
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def external_url(value: str) -> str:
|
|
138
|
+
"""Add attributes to external URLs a tags"""
|
|
139
|
+
if value.startswith(('http://', 'https://')):
|
|
140
|
+
return '\
|
|
141
|
+
referrerpolicy="no-referrer" rel="noopener noreferrer" target="_blank"'
|
|
142
|
+
else:
|
|
143
|
+
return ''
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def page_name(event, site: dict):
|
|
147
|
+
"""
|
|
148
|
+
Return the page name that event belongs to:
|
|
149
|
+
"À venir", "2020", "2019", etc.
|
|
150
|
+
"""
|
|
151
|
+
event_date = event['date'].date()
|
|
152
|
+
return "À venir" if event_date >= site['today'] else event_date.year
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def event_selector(event, page):
|
|
156
|
+
"""Return true if event should be display in page"""
|
|
157
|
+
site = page.conf.site
|
|
158
|
+
return (not page.paginator
|
|
159
|
+
or page.name == page_name(event, site))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def recent_news(news: dict, site: dict) -> bool:
|
|
163
|
+
"""Return True if news is less than 180 days ago"""
|
|
164
|
+
try:
|
|
165
|
+
date = news['date'].date()
|
|
166
|
+
except AttributeError:
|
|
167
|
+
date = news['date']
|
|
168
|
+
return date >= site['today'] - timedelta(days=180)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def news_page_name(news, site: dict):
|
|
172
|
+
"""
|
|
173
|
+
Return the page name that event belongs to:
|
|
174
|
+
"Récentes", "2020", "2019", etc.
|
|
175
|
+
"""
|
|
176
|
+
# Handle both datetime and date objects
|
|
177
|
+
try:
|
|
178
|
+
year = news['date'].date().year
|
|
179
|
+
except AttributeError:
|
|
180
|
+
year = news['date'].year
|
|
181
|
+
return "Récentes" if recent_news(news, site) else year
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def news_selector(news, page):
|
|
185
|
+
"""Return true if news should be display in page"""
|
|
186
|
+
return (not page.paginator
|
|
187
|
+
or page.name == news_page_name(news, page.website.conf.site))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@jinja2.pass_context
|
|
191
|
+
def news_page(context, site_news):
|
|
192
|
+
"""
|
|
193
|
+
- keep same year
|
|
194
|
+
- (reverse) sort by date
|
|
195
|
+
"""
|
|
196
|
+
page = context['page']
|
|
197
|
+
site = context['site']
|
|
198
|
+
if page.paginator:
|
|
199
|
+
name_selection = (news for news in site_news
|
|
200
|
+
if news_page_name(news, site) == page.name)
|
|
201
|
+
else:
|
|
202
|
+
name_selection = site_news
|
|
203
|
+
return sort_by_date(name_selection, reverse=True)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@jinja2.pass_context
|
|
207
|
+
def dropdown_items(context, nav_items):
|
|
208
|
+
"""Return the dropdown_items if item is a dict else {}"""
|
|
209
|
+
page = context['page']
|
|
210
|
+
nav_item = nav_items[page.menu_item]
|
|
211
|
+
return nav_item if isinstance(nav_item, dict) else {}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_fullname(teams: dict, shortname: str) -> str:
|
|
215
|
+
"""Return fullname from shortname"""
|
|
216
|
+
try:
|
|
217
|
+
return teams[shortname]['fullname']
|
|
218
|
+
except KeyError:
|
|
219
|
+
# team entry does not belongs to research teams list
|
|
220
|
+
return shortname
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def real_teams(sem_teams: list) -> bool:
|
|
224
|
+
"""Return True if team is not in exclude names"""
|
|
225
|
+
return bool(sem_teams) and sem_teams[0].upper() not in {
|
|
226
|
+
'COLLOQUIUM', 'ENTR', 'AUTRE'}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@jinja2.pass_context
|
|
230
|
+
def teams(context, sem_teams) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Format team names such as:
|
|
233
|
+
Team1,
|
|
234
|
+
Team1 and Team2
|
|
235
|
+
Team1, Team2 and Team3
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
t_dict = context['teams']
|
|
239
|
+
|
|
240
|
+
if sem_teams:
|
|
241
|
+
if len(sem_teams) == 1:
|
|
242
|
+
return get_fullname(t_dict, sem_teams[0])
|
|
243
|
+
else:
|
|
244
|
+
return "{} et {}".format(', '.join(get_fullname(t_dict, team)
|
|
245
|
+
for team in sem_teams[:-1]),
|
|
246
|
+
get_fullname(t_dict, sem_teams[-1]))
|
|
247
|
+
else:
|
|
248
|
+
return ''
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@jinja2.pass_context
|
|
252
|
+
def organizing_teams(context, sem_teams) -> str:
|
|
253
|
+
"""
|
|
254
|
+
Format team names such as:
|
|
255
|
+
organisé par l'équipe Team1,
|
|
256
|
+
organisé par les équipes Team1 et Team2
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
if not real_teams(sem_teams):
|
|
260
|
+
return ''
|
|
261
|
+
|
|
262
|
+
t_dict = context['teams']
|
|
263
|
+
|
|
264
|
+
def link_team(team: str) -> str:
|
|
265
|
+
"""Return a html link with the team fullname"""
|
|
266
|
+
if team in t_dict:
|
|
267
|
+
target = url(context, f"teams/{team.lower()}.html")
|
|
268
|
+
return f'''\
|
|
269
|
+
<a href="{target}">{get_fullname(t_dict, team)}</a>\
|
|
270
|
+
'''
|
|
271
|
+
else:
|
|
272
|
+
return team
|
|
273
|
+
|
|
274
|
+
if sem_teams:
|
|
275
|
+
if len(sem_teams) == 1:
|
|
276
|
+
return f"organisé par l'équipe {link_team(sem_teams[0])}"
|
|
277
|
+
else:
|
|
278
|
+
return "organisé par les équipes {} et {}".format(
|
|
279
|
+
', '.join(link_team(team) for team in sem_teams[:-1]),
|
|
280
|
+
link_team(sem_teams[-1]))
|
|
281
|
+
else:
|
|
282
|
+
return ''
|