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.
Files changed (58) hide show
  1. sgelt/__init__.py +11 -0
  2. sgelt/__main__.py +6 -0
  3. sgelt/__version__.py +1 -0
  4. sgelt/calendar.py +105 -0
  5. sgelt/config.py +179 -0
  6. sgelt/data.py +42 -0
  7. sgelt/filters.py +282 -0
  8. sgelt/main.py +94 -0
  9. sgelt/mdextend.py +16 -0
  10. sgelt/mdparse.py +138 -0
  11. sgelt/pages.py +973 -0
  12. sgelt/py.typed +0 -0
  13. sgelt/serve.py +81 -0
  14. sgelt/sgelt.py +330 -0
  15. sgelt/theme/LICENSE +21 -0
  16. sgelt/theme/css/materialize.css +9058 -0
  17. sgelt/theme/css/materialize.min.css +13 -0
  18. sgelt/theme/css/style.css +695 -0
  19. sgelt/theme/img/favicon.ico +0 -0
  20. sgelt/theme/img/iss.jpg +0 -0
  21. sgelt/theme/img/logo_jinja.png +0 -0
  22. sgelt/theme/img/logo_materialize.png +0 -0
  23. sgelt/theme/img/logo_pi.png +0 -0
  24. sgelt/theme/img/logo_python.png +0 -0
  25. sgelt/theme/js/delaunay.js +65 -0
  26. sgelt/theme/js/init.js +30 -0
  27. sgelt/theme/js/lunr-search-result.js +106 -0
  28. sgelt/theme/js/lunr.js +3573 -0
  29. sgelt/theme/js/materialize.js +12337 -0
  30. sgelt/theme/js/materialize.min.js +6 -0
  31. sgelt/theme/js/parallax.js +412 -0
  32. sgelt/theme/js/parallax.min.js +6 -0
  33. sgelt/theme/templates/actualites-bib.html +13 -0
  34. sgelt/theme/templates/actualites.html +39 -0
  35. sgelt/theme/templates/agenda.html +25 -0
  36. sgelt/theme/templates/article.html +44 -0
  37. sgelt/theme/templates/base.html +126 -0
  38. sgelt/theme/templates/conference.html +119 -0
  39. sgelt/theme/templates/equipe.html +14 -0
  40. sgelt/theme/templates/index.html +85 -0
  41. sgelt/theme/templates/macros.jinja.html +182 -0
  42. sgelt/theme/templates/page.html +24 -0
  43. sgelt/theme/templates/partial/date_place.html +17 -0
  44. sgelt/theme/templates/partial/footer.html +54 -0
  45. sgelt/theme/templates/partial/nav.html +46 -0
  46. sgelt/theme/templates/partial/news_button.html +16 -0
  47. sgelt/theme/templates/search.html +45 -0
  48. sgelt/theme/templates/seminaire.html +56 -0
  49. sgelt/theme/templates/seminaires.html +56 -0
  50. sgelt/theme/templates/soutenance.html +58 -0
  51. sgelt/utils.py +208 -0
  52. sgelt/watch.py +81 -0
  53. sgelt-4.8.1.dist-info/LICENSE +21 -0
  54. sgelt-4.8.1.dist-info/METADATA +117 -0
  55. sgelt-4.8.1.dist-info/RECORD +58 -0
  56. sgelt-4.8.1.dist-info/WHEEL +5 -0
  57. sgelt-4.8.1.dist-info/entry_points.txt +2 -0
  58. 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
@@ -0,0 +1,6 @@
1
+ """Enable to run with `python -m sgelt`"""
2
+
3
+ from .main import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
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 ''