-
Notifications
You must be signed in to change notification settings - Fork 411
/
offline.py
394 lines (341 loc) · 18.6 KB
/
offline.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# This file is part of Indico.
# Copyright (C) 2002 - 2024 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.
import inspect
import itertools
import os
import posixpath
import re
from pathlib import Path
from tempfile import NamedTemporaryFile
from zipfile import ZipFile
from flask import g, request, session
from flask.helpers import get_root_path
from werkzeug.utils import secure_filename
from indico.core.config import config
from indico.core.plugins import plugin_engine
from indico.legacy.pdfinterface.conference import ProgrammeToPDF
from indico.legacy.pdfinterface.latex import AbstractBook, ContribsToPDF, ContribToPDF
from indico.modules.attachments.models.attachments import AttachmentType
from indico.modules.attachments.models.folders import AttachmentFolder
from indico.modules.events.contributions.controllers.display import (RHAuthorList, RHContributionAuthor,
RHContributionDisplay, RHContributionList,
RHSpeakerList, RHSubcontributionDisplay)
from indico.modules.events.contributions.ical import contribution_to_ical
from indico.modules.events.controllers.display import RHDisplayPrivacyPolicy
from indico.modules.events.layout.models.menu import MenuEntryType
from indico.modules.events.layout.util import menu_entries_for_event
from indico.modules.events.models.events import EventType
from indico.modules.events.registration.controllers.display import RHParticipantList
from indico.modules.events.sessions.controllers.display import RHDisplaySession
from indico.modules.events.sessions.ical import session_to_ical
from indico.modules.events.sessions.util import get_session_timetable_pdf
from indico.modules.events.static.util import collect_static_files, override_request_endpoint, rewrite_css_urls
from indico.modules.events.static.views import (WPStaticAuthorList, WPStaticConferenceDisplay,
WPStaticConferencePrivacyDisplay, WPStaticConferenceProgram,
WPStaticContributionDisplay, WPStaticContributionList,
WPStaticCustomPage, WPStaticDisplayRegistrationParticipantList,
WPStaticSessionDisplay, WPStaticSimpleEventDisplay, WPStaticSpeakerList,
WPStaticSubcontributionDisplay, WPStaticTimetable)
from indico.modules.events.timetable.controllers.display import RHTimetable
from indico.modules.events.timetable.util import get_timetable_offline_pdf_generator
from indico.modules.events.tracks.controllers import RHDisplayTracks
from indico.util.fs import chmod_umask
from indico.util.string import strip_tags
from indico.web.assets.vars_js import generate_global_file, generate_i18n_file, generate_user_file
from indico.web.flask.util import url_for
from indico.web.rh import RH
def create_static_site(rh, event):
"""Create a static (offline) version of an Indico event.
:param rh: Request handler object
:param event: Event in question
:return: Path to the resulting ZIP file
"""
try:
g.static_site = True
g.rh = rh
cls = StaticEventCreator if event.type_ in (EventType.lecture, EventType.meeting) else StaticConferenceCreator
return cls(rh, event).create()
finally:
g.static_site = False
g.rh = None
def _normalize_path(path):
return secure_filename(strip_tags(path))
class StaticEventCreator:
"""Define process which generates a static (offline) version of an Indico event."""
def __init__(self, rh, event):
self._rh = rh
self.event = event
self._display_tz = self.event.display_tzinfo.zone
self._zip_file = None
self._content_dir = _normalize_path(f'OfflineWebsite-{event.title}')
self._web_dir = os.path.join(get_root_path('indico'), 'web')
self._static_dir = os.path.join(self._web_dir, 'static')
def create(self):
"""Trigger the creation of a ZIP file containing the site."""
temp_file = NamedTemporaryFile(prefix=f'static-{self.event.id}-', suffix='.zip', dir=config.TEMP_DIR,
delete=False)
self._zip_file = ZipFile(temp_file.name, 'w', allowZip64=True)
with collect_static_files() as used_assets:
# create the home page html
html = self._create_home()
# Mathjax plugins can only be known in runtime
self._copy_folder(os.path.join(self._content_dir, 'static', 'dist', 'js', 'mathjax'),
os.path.join(self._static_dir, 'dist', 'js', 'mathjax'))
# Materials and additional pages
self._copy_all_material()
self._create_other_pages()
# Create index.html file (main page for the event)
index_path = os.path.join(self._content_dir, 'index.html')
self._zip_file.writestr(index_path, html)
self._write_generated_js()
# Copy static assets to ZIP file
self._copy_static_files(used_assets)
self._copy_plugin_files(used_assets)
if config.CUSTOMIZATION_DIR:
self._copy_customization_files(used_assets)
chmod_umask(temp_file.name)
self._zip_file.close()
return temp_file.name
def _write_generated_js(self):
global_js = generate_global_file()
user_js = generate_user_file()
i18n_js = f'window.TRANSLATIONS = {generate_i18n_file(session.lang)};'
react_i18n_js = f'window.REACT_TRANSLATIONS = {generate_i18n_file(session.lang, react=True)};'
gen_path = os.path.join(self._content_dir, 'assets')
self._zip_file.writestr(os.path.join(gen_path, 'js-vars', 'global.js'), global_js)
self._zip_file.writestr(os.path.join(gen_path, 'js-vars', 'user.js'), user_js)
self._zip_file.writestr(os.path.join(gen_path, 'i18n', session.lang + '.js'), i18n_js)
self._zip_file.writestr(os.path.join(gen_path, 'i18n', session.lang + '-react.js'), react_i18n_js)
def _copy_static_files(self, used_assets):
# add favicon
used_assets.add('static/images/indico.ico')
# assets
css_files = {url for url in used_assets if re.match(r'static/dist/.*\.css$', url)}
for file_path in css_files:
css_data = Path(self._web_dir, file_path).read_text()
rewritten_css, used_urls, __ = rewrite_css_urls(self.event, css_data)
used_assets |= used_urls
self._zip_file.writestr(os.path.join(self._content_dir, file_path), rewritten_css)
for file_path in used_assets - css_files:
if not re.match('^static/(images|fonts|dist)/(?!js/ckeditor/)', file_path):
continue
self._copy_file(os.path.join(self._content_dir, file_path),
os.path.join(self._web_dir, file_path))
def _copy_plugin_files(self, used_assets):
css_files = {url for url in used_assets if re.match(r'static/plugins/.*\.css$', url)}
for file_path in css_files:
plugin_name, path = re.match(r'static/plugins/([^/]+)/(.+.css)', file_path).groups()
plugin = plugin_engine.get_plugin(plugin_name)
css_data = Path(plugin.root_path, 'static', path).read_text()
rewritten_css, used_urls, __ = rewrite_css_urls(self.event, css_data)
used_assets |= used_urls
self._zip_file.writestr(os.path.join(self._content_dir, file_path), rewritten_css)
for file_path in used_assets - css_files:
match = re.match(r'static/plugins/([^/]+)/(.+)', file_path)
if not match:
continue
plugin_name, path = match.groups()
plugin = plugin_engine.get_plugin(plugin_name)
self._copy_file(os.path.join(self._content_dir, file_path),
os.path.join(plugin.root_path, 'static', path))
def _strip_custom_prefix(self, url):
# strip the 'static/custom/' prefix from the given url/path
return '/'.join(url.split('/')[2:])
def _copy_customization_files(self, used_assets):
css_files = {url for url in used_assets if re.match(r'static/custom/.*\.css$', url)}
for file_path in css_files:
css_data = Path(os.path.join(config.CUSTOMIZATION_DIR, self._strip_custom_prefix(file_path))).read_text()
rewritten_css, used_urls, __ = rewrite_css_urls(self.event, css_data)
used_assets |= used_urls
self._zip_file.writestr(os.path.join(self._content_dir, file_path), rewritten_css)
for file_path in used_assets - css_files:
if not file_path.startswith('static/custom/'):
continue
self._copy_file(os.path.join(self._content_dir, file_path),
os.path.join(config.CUSTOMIZATION_DIR, self._strip_custom_prefix(file_path)))
def _create_home(self):
return WPStaticSimpleEventDisplay(self._rh, self.event, self.event.theme).display()
def _create_other_pages(self):
pass
def _copy_all_material(self):
self._add_material(self.event, '')
for contrib in self.event.contributions:
if not contrib.can_access(None):
continue
self._add_material(contrib, '%s-contribution' % contrib.friendly_id)
for sc in contrib.subcontributions:
self._add_material(sc, '%s-subcontribution' % sc.friendly_id)
for session_ in self.event.sessions:
if not session_.can_access(None):
continue
self._add_material(session_, '%s-session' % session_.friendly_id)
def _add_material(self, target, type_):
for folder in AttachmentFolder.get_for_linked_object(target, preload_event=True):
for attachment in folder.attachments:
if not attachment.can_access(None):
continue
if attachment.type == AttachmentType.file:
dst_path = posixpath.join(self._content_dir, 'material', type_,
f'{attachment.id}-{attachment.file.filename}')
with attachment.file.get_local_path() as file_path:
self._copy_file(dst_path, file_path)
def _copy_file(self, dest, src):
"""Copy a file from a source path to a destination inside the ZIP."""
self._zip_file.write(src, dest)
def _copy_folder(self, dest, src):
for root, _dirs, files in os.walk(src):
dst_dirpath = os.path.join(dest, os.path.relpath(root, src))
for filename in files:
src_filepath = os.path.join(src, root, filename)
self._zip_file.write(src_filepath, os.path.join(dst_dirpath, filename))
class StaticConferenceCreator(StaticEventCreator):
def __init__(self, rh, event):
super().__init__(rh, event)
# Menu entries we want to include in the offline version.
# Those which are backed by a WP class get their name from that class;
# the others are simply hardcoded.
self._menu_offline_items = {
'overview': None,
'abstracts_book': None
}
rhs = {
RHParticipantList: WPStaticDisplayRegistrationParticipantList,
RHContributionList: WPStaticContributionList,
RHAuthorList: WPStaticAuthorList,
RHSpeakerList: WPStaticSpeakerList,
RHTimetable: WPStaticTimetable,
RHDisplayTracks: WPStaticConferenceProgram,
RHDisplayPrivacyPolicy: WPStaticConferencePrivacyDisplay,
}
for rh_cls, wp in rhs.items():
rh = rh_cls()
rh.view_class = wp
if rh_cls is RHTimetable:
rh.view_class_simple = WPStaticSimpleEventDisplay
self._menu_offline_items[wp.menu_entry_name] = rh
def _create_home(self):
if self.event.has_stylesheet:
css, used_urls, used_images = rewrite_css_urls(self.event, self.event.stylesheet)
g.used_url_for_assets |= used_urls
self._zip_file.writestr(os.path.join(self._content_dir, 'custom.css'), css)
for image_file in used_images:
with image_file.open() as f:
self._zip_file.writestr(os.path.join(self._content_dir,
f'images/{image_file.id}-{image_file.filename}'),
f.read())
if self.event.has_logo:
self._zip_file.writestr(os.path.join(self._content_dir, 'logo.png'), self.event.logo)
return WPStaticConferenceDisplay(self._rh, self.event).display()
def _create_other_pages(self):
# Getting all menu items
self._get_menu_items()
# Getting conference timetable in PDF
self._add_pdf(self.event, 'timetable.export_default_pdf',
get_timetable_offline_pdf_generator(self.event))
if config.LATEX_ENABLED:
# Generate contributions in PDF
self._add_pdf(self.event, 'contributions.contribution_list_pdf', ContribsToPDF, event=self.event,
contribs=[c for c in self.event.contributions if c.can_access(None)])
# Getting specific pages for contributions
for contrib in self.event.contributions:
if not contrib.can_access(None):
continue
self._get_contrib(contrib)
# Getting specific pages for subcontributions
for subcontrib in contrib.subcontributions:
self._get_sub_contrib(subcontrib)
for session_ in self.event.sessions:
if not session_.can_access(None):
continue
self._get_session(session_)
def _get_menu_items(self):
entries = menu_entries_for_event(self.event)
visible_entries = [e for e in itertools.chain(entries, *(e.children for e in entries)) if e.is_visible]
for entry in visible_entries:
if entry.type == MenuEntryType.page:
self._get_custom_page(entry.page)
elif entry.type == MenuEntryType.internal_link:
self._get_builtin_page(entry)
def _get_builtin_page(self, entry):
obj = self._menu_offline_items.get(entry.name)
if isinstance(obj, RH):
request.view_args = {'event_id': self.event.id}
with override_request_endpoint(obj.view_class.endpoint):
obj._process_args()
self._add_page(obj._process(), obj.view_class.endpoint, self.event)
if entry.name == 'abstracts_book' and config.LATEX_ENABLED:
self._add_pdf(self.event, 'abstracts.export_boa', AbstractBook, event=self.event)
if entry.name == 'program':
self._add_pdf(self.event, 'tracks.program_pdf', ProgrammeToPDF, event=self.event)
def _get_custom_page(self, page):
html = WPStaticCustomPage.render_template('page.html', self.event, page=page)
self._add_page(html, 'event_pages.page_display', page)
def _get_url(self, uh_or_endpoint, target, **params):
if isinstance(uh_or_endpoint, str):
return url_for(uh_or_endpoint, target, **params)
else:
return str(uh_or_endpoint.getStaticURL(target, **params))
def _add_page(self, html, uh_or_endpoint, target=None, **params):
url = self._get_url(uh_or_endpoint, target, **params)
fname = os.path.join(self._content_dir, url)
self._zip_file.writestr(fname, html)
def _add_from_rh(self, rh_class, view_class, params, url_for_target):
rh = rh_class()
rh.view_class = view_class
request.view_args = params
with override_request_endpoint(rh.view_class.endpoint):
rh._process_args()
html = rh._process()
self._add_page(html, rh.view_class.endpoint, url_for_target)
def _get_contrib(self, contrib):
self._add_from_rh(RHContributionDisplay, WPStaticContributionDisplay,
{'event_id': self.event.id, 'contrib_id': contrib.id},
contrib)
if config.LATEX_ENABLED:
self._add_pdf(contrib, 'contributions.export_pdf', ContribToPDF, contrib=contrib)
for author in contrib.primary_authors:
self._get_author(contrib, author)
for author in contrib.secondary_authors:
self._get_author(contrib, author)
if contrib.timetable_entry:
self._add_file(contribution_to_ical(contrib), 'contributions.export_ics', contrib)
def _get_sub_contrib(self, subcontrib):
self._add_from_rh(RHSubcontributionDisplay, WPStaticSubcontributionDisplay,
{'event_id': self.event.id, 'contrib_id': subcontrib.contribution.id,
'subcontrib_id': subcontrib.id}, subcontrib)
def _get_author(self, contrib, author):
rh = RHContributionAuthor()
params = {'event_id': self.event.id, 'contrib_id': contrib.id, 'person_id': author.id}
request.view_args = params
with override_request_endpoint('contributions.display_author'):
rh._process_args()
html = rh._process()
self._add_page(html, 'contributions.display_author', self.event, contrib_id=contrib.id, person_id=author.id)
def _get_session(self, session):
self._add_from_rh(RHDisplaySession, WPStaticSessionDisplay,
{'event_id': self.event.id, 'session_id': session.id}, session)
pdf = get_session_timetable_pdf(session, tz=self._display_tz)
self._add_pdf(session, 'sessions.export_session_timetable', pdf)
self._add_file(session_to_ical(session), 'sessions.export_ics', session)
def _add_pdf(self, target, uh_or_endpoint, generator_class_or_instance, **kwargs):
if inspect.isclass(generator_class_or_instance):
pdf = generator_class_or_instance(tz=self._display_tz, **kwargs)
else:
pdf = generator_class_or_instance
if hasattr(pdf, 'getPDFBin'):
# Got legacy reportlab PDF generator instead of the LaTex-based one
self._add_file(pdf.getPDFBin(), uh_or_endpoint, target)
else:
with open(pdf.generate(), 'rb') as f:
self._add_file(f, uh_or_endpoint, target)
def _add_file(self, file_like_or_str, uh_or_endpoint, target):
if isinstance(file_like_or_str, (str, bytes)):
content = file_like_or_str
else:
content = file_like_or_str.read()
filename = os.path.join(self._content_dir, self._get_url(uh_or_endpoint, target))
self._zip_file.writestr(filename, content)