server.py 18.1 KB
Newer Older
1
from flask import Flask, g, request, url_for, redirect, session, render_template, flash, Response
2
from werkzeug.routing import Rule
3
from functools import wraps
Julian Rother's avatar
Julian Rother committed
4
from datetime import date, timedelta, datetime, time, MINYEAR
5
import threading
6
import os
7
import sys
Julian Rother's avatar
Julian Rother committed
8
import hashlib
9
import random
10
import sched
11

12
app = Flask(__name__)
13

Andreas Valder's avatar
Andreas Valder committed
14
15
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
Julian Rother's avatar
Julian Rother committed
16
app.add_template_global(random.randint, name='randint')
17
18
app.add_template_global(datetime, name='datetime')
app.add_template_global(timedelta, name='timedelta')
Andreas Valder's avatar
Andreas Valder committed
19

20
21
scheduler = sched.scheduler()
def run_scheduler():
Andreas Valder's avatar
Andreas Valder committed
22
	import time
23
	time.sleep(1) # UWSGI does weird things on startup
24
25
	while True:
		scheduler.run()
26
		time.sleep(10)
27

28
29
30
def sched_func(delay, priority=0, firstdelay=None, args=[], kargs={}):
	if firstdelay == None:
		firstdelay = random.randint(1, 120)
31
32
33
34
35
	def wrapper(func):
		def sched_wrapper():
			with app.test_request_context():
				func(*args, *kargs)
			scheduler.enter(delay, priority, sched_wrapper)
36
		scheduler.enter(firstdelay, priority, sched_wrapper)
37
38
39
40
		return func
	return wrapper

threading.Thread(target=run_scheduler, daemon=True).start()
41

42
config = app.config
43
config.from_pyfile('config.py.example', silent=True)
44
45
46
if sys.argv[0].endswith('run.py'): 
	config['SQLITE_INIT_DATA'] = True
	config['DEBUG'] = True
47
config.from_pyfile('config.py', silent=True)
Andreas Valder's avatar
Andreas Valder committed
48
49
if config['DEBUG']:
	app.jinja_env.auto_reload = True
50
51
if not config.get('SECRET_KEY', None):
	config['SECRET_KEY'] = os.urandom(24)
Julian Rother's avatar
Julian Rother committed
52

Julian Rother's avatar
Julian Rother committed
53
from db import query, modify, searchquery, ldapauth, ldapget
Julian Rother's avatar
Julian Rother committed
54

55
mod_endpoints = []
Julian Rother's avatar
Julian Rother committed
56

Julian Rother's avatar
Cleanup    
Julian Rother committed
57
@app.template_global()
58
59
60
61
def ismod(*args):
	return ('user' in session)

def mod_required(func):
62
	mod_endpoints.append(func.__name__)
63
64
	@wraps(func)
	def decorator(*args, **kwargs):
65
		if not ismod():
66
67
68
69
70
71
			flash('Diese Funktion ist nur für Moderatoren verfügbar!')
			return redirect(url_for('login', ref=request.url))
		else:
			return func(*args, **kwargs)
	return decorator

72
app.jinja_env.globals['navbar'] = []
73
74
75
76
77
# iconlib can be 'bootstrap'
# ( see: http://getbootstrap.com/components/#glyphicons )
# or 'fa'
# ( see: http://fontawesome.io/icons/ )
def register_navbar(name, iconlib='bootstrap', icon=None):
78
	def wrapper(func):
79
		endpoint = func.__name__
80
		app.jinja_env.globals['navbar'].append((endpoint, name, iconlib, icon, not endpoint in mod_endpoints))
81
82
83
		return func
	return wrapper

Julian Rother's avatar
Cleanup    
Julian Rother committed
84
85
86
87
def render_endpoint(endpoint, flashtext=None, **kargs):
	if flashtext:
		flash(flashtext)
	# request.endpoint is used for navbar highlighting
88
	request.url_rule = Rule(request.path, endpoint=endpoint)
Julian Rother's avatar
Cleanup    
Julian Rother committed
89
90
	return app.view_functions[endpoint](**kargs)

91
92
93
94
95
96
97
def handle_errors(endpoint, text, code, *errors, **epargs):
	def wrapper(func):
		@wraps(func)
		def decorator(*args, **kwargs):
			try:
				return func(*args, **kwargs)
			except errors:
Julian Rother's avatar
Julian Rother committed
98
99
100
101
				if endpoint:
					return render_endpoint(endpoint, text, **epargs), code
				else:
					return text, code
102
103
104
		return decorator
	return wrapper

Julian Rother's avatar
Cleanup    
Julian Rother committed
105
106
@app.errorhandler(404)
def handle_not_found(e):
107
	return render_endpoint('index', 'Diese Seite existiert nicht!'), 404
Julian Rother's avatar
Cleanup    
Julian Rother committed
108

Julian Rother's avatar
Julian Rother committed
109
@app.template_filter(name='semester')
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def human_semester(s, long=False):
	if not s or s == 'zeitlos' or len(s) != 6:
		return 'Zeitlos'
	year = s[0:4]
	semester = s[4:6].upper()
	if not year.isdigit() or semester not in ['SS', 'WS']:
		print('Invalid semester string "%s"'%s)
		return '??'
	if not long:
		return semester+year[2:]
	elif semester == 'SS':
		return 'Sommersemester %s'%year
	else:
		return 'Wintersemester %s/%s'%(year, str(int(year)+1)[2:])
Julian Rother's avatar
Julian Rother committed
124
125
126

@app.template_filter(name='date')
def human_date(d):
Andreas Valder's avatar
Andreas Valder committed
127
	return d.strftime('%d.%m.%Y')
Julian Rother's avatar
Julian Rother committed
128

Andreas Valder's avatar
Andreas Valder committed
129
@app.template_filter(name='time')
130
def human_time(d):
Andreas Valder's avatar
Andreas Valder committed
131
132
	return d.strftime('%H:%M')

Julian Rother's avatar
Julian Rother committed
133
134
135
136
@app.template_filter()
def rfc3339(d):
	return d.strftime('%Y-%m-%dT%H:%M:%S+02:00')

137
138
@app.template_global()
def get_announcements(minlevel=0):
139
140
	offset = timedelta()
	if ismod():
141
		offset = timedelta(hours=24)
142
	return query('SELECT * FROM announcements WHERE NOT deleted AND (time_expire ISNULL OR time_expire > ?) AND (? OR (visible AND time_publish < ?)) AND level >= ? ORDER BY level DESC', datetime.now()-offset, ismod(), datetime.now(), minlevel)
143

144
145
146
147
148
@app.template_filter()
def fixnl(s):
	# To be remove, as soon as db schema is cleaned-up
	return str(s).replace('\n', '<br>')

149
@app.route('/')
150
@register_navbar('Home', icon='home')
151
def index():
152
153
	start = date.today() - timedelta(days=1)
	end = start + timedelta(days=7)
154
155
	upcomming = query('''
		SELECT lectures.*, "course" AS sep, courses.*
Andreas Valder's avatar
Andreas Valder committed
156
157
		FROM lectures
		JOIN courses ON (lectures.course_id = courses.id)
158
159
		WHERE (time > ?) AND (time < ?) and lectures.visible and courses.visible and courses.listed
		ORDER BY time ASC LIMIT 30''',start,end)
Andreas Valder's avatar
Andreas Valder committed
160
161
162
	for i in upcomming:
		i['date'] = i['time'].date()
	latestvideos=query('''
163
		SELECT lectures.*, "course" AS sep, courses.*
Andreas Valder's avatar
Andreas Valder committed
164
165
166
167
168
		FROM lectures
		LEFT JOIN videos ON (videos.lecture_id = lectures.id)
		LEFT JOIN courses on (courses.id = lectures.course_id)
		WHERE (? OR (courses.visible AND courses.listed AND lectures.visible AND videos.visible))
		GROUP BY videos.lecture_id
169
		ORDER BY MAX(videos.time_updated) DESC
Andreas Valder's avatar
Andreas Valder committed
170
		LIMIT 6	''',ismod())
171
172
	featured = query('SELECT * FROM featured WHERE NOT deleted AND (? OR visible)', ismod())
	return render_template('index.html', latestvideos=latestvideos, upcomming=upcomming, featured=featured)
173

174
@app.route('/course')
175
@register_navbar('Videos', icon='film')
176
def courses():
177
	courses = query('SELECT * FROM courses WHERE (? OR (visible AND listed)) ORDER BY title', ismod())
178
179
180
	for course in courses:
		if course['semester'] == '':
			course['semester'] = 'zeitlos'
Andreas Valder's avatar
Andreas Valder committed
181
	groupedby = request.args.get('groupedby')
Julian Rother's avatar
Cleanup    
Julian Rother committed
182
	if groupedby not in ['title', 'semester', 'organizer']:
Andreas Valder's avatar
Andreas Valder committed
183
		groupedby = 'semester'
184
	return render_template('courses.html', courses=courses, groupedby=groupedby)
Andreas Valder's avatar
Andreas Valder committed
185

186
187
@app.route('/course/<handle>')
@app.route('/course/<int:id>')
188
@handle_errors('courses', 'Diese Veranstaltung existiert nicht!', 404, IndexError)
189
190
def course(id=None, handle=None):
	if id:
191
		course = query('SELECT * FROM courses WHERE id = ? AND (? OR visible)', id, ismod())[0]
192
	else:
193
		course = query('SELECT * FROM courses WHERE handle = ? AND (? OR visible)', handle, ismod())[0]
194
195
196
	course['auth'] = query('SELECT * FROM auth WHERE course_id = ? ORDER BY auth_type', course['id'])
	auths = query('SELECT auth.* FROM auth JOIN lectures ON (auth.lecture_id = lectures.id) WHERE lectures.course_id = ? ORDER BY auth.auth_type', course['id'])
	lectures = query('SELECT * FROM lectures WHERE course_id = ? AND (? OR visible) ORDER BY time, duration DESC', course['id'], ismod())
197
198
	for lecture in lectures:
		lecture['auth'] = []
199
		lecture['course'] = course
200
201
202
		for auth in auths:
			if auth['lecture_id'] == lecture['id']:
				lecture['auth'].append(auth)
Andreas Valder's avatar
Andreas Valder committed
203
	videos = query('''
204
			SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, formats.description AS format_description, formats.player_prio, formats.prio
Andreas Valder's avatar
Andreas Valder committed
205
206
207
208
209
210
			FROM videos
			JOIN lectures ON (videos.lecture_id = lectures.id)
			JOIN formats ON (videos.video_format = formats.id)
			JOIN courses ON (lectures.course_id = courses.id)
			WHERE lectures.course_id= ? AND (? OR videos.visible)
			ORDER BY lectures.time, formats.prio DESC
211
212
			''', course['id'], ismod())
	return render_template('course.html', course=course, lectures=lectures, videos=videos)
Andreas Valder's avatar
Andreas Valder committed
213

Andreas Valder's avatar
Andreas Valder committed
214
@app.route('/faq')
215
@register_navbar('FAQ', icon='question-sign')
Andreas Valder's avatar
Andreas Valder committed
216
def faq():
217
	return render_template('faq.html')
Andreas Valder's avatar
Andreas Valder committed
218

219
@app.route('/play/<int:id>')
Andreas Valder's avatar
Andreas Valder committed
220
@app.route('/embed/<int:id>', endpoint='embed')
221
@handle_errors('course', 'Diese Vorlesung existiert nicht!', 404, IndexError)
222
def lecture(id):
Andreas Valder's avatar
Andreas Valder committed
223
224
225
226
227
	lecture = query('SELECT * FROM lectures WHERE id = ? AND (? OR visible)', id, ismod())[0]
	videos = query('''
			SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, formats.description AS format_description, formats.player_prio, formats.prio
			FROM videos
			JOIN formats ON (videos.video_format = formats.id)
228
229
230
231
			JOIN courses ON (courses.id = ?)
			WHERE videos.lecture_id = ? AND (? OR videos.visible)
			ORDER BY formats.prio DESC
			''', lecture['course_id'], lecture['id'], ismod())
232
233
	if not videos:
		flash('Zu dieser Vorlesung wurden noch keine Videos veröffentlicht!')
Andreas Valder's avatar
Andreas Valder committed
234
235
	course = query('SELECT * FROM courses WHERE id = ? AND (? OR (visible AND listed))', lecture['course_id'], ismod())
	if not course:
236
		return render_endpoint('course', 'Diese Veranstaltung existiert nicht!'), 404
237
	chapters = query('SELECT * FROM chapters WHERE lecture_id = ? AND NOT deleted AND (? OR visible) ORDER BY time ASC', id, ismod())
Andreas Valder's avatar
Andreas Valder committed
238
	return render_template('embed.html' if request.endpoint == 'embed' else 'lecture.html', course=course, lecture=lecture, videos=videos, chapters=chapters)
Andreas Valder's avatar
Andreas Valder committed
239

240
241
242
243
244
245
246

@app.route('/search')
def search():
	if 'q' not in request.args:
		return redirect(url_for('index'))
	q = request.args['q']
	courses = searchquery(q, '*', ['title', 'short', 'organizer', 'subject', 'description'],
247
			'courses', 'WHERE (? OR (visible AND listed)) GROUP BY id ORDER BY _score DESC, semester DESC LIMIT 20', ismod())
248
	lectures = searchquery(q, 'lectures.*, courses.visible AS coursevisible, courses.listed, "course" AS sep, courses.*',
249
250
			['lectures.title', 'lectures.comment', 'lectures.speaker', 'courses.short'],
			'lectures LEFT JOIN courses on (courses.id = lectures.course_id)',
251
			'WHERE (? OR (coursevisible AND listed AND visible)) GROUP BY id ORDER BY _score DESC, time DESC LIMIT 30', ismod())
252
	return render_template('search.html', searchtext=request.args['q'], courses=courses, lectures=lectures)
Andreas Valder's avatar
Andreas Valder committed
253

254
255
256
def check_mod(user, groups):
	return user and 'users' in groups

257
@app.route('/login', methods=['GET', 'POST'])
Julian Rother's avatar
Julian Rother committed
258
def login():
259
260
	if request.method == 'GET':
		return render_template('login.html')
Julian Rother's avatar
Julian Rother committed
261
	user, groups = ldapauth(request.form.get('user'), request.form.get('password'))
262
	if not check_mod(user, groups):
263
		flash('Login fehlgeschlagen!')
264
265
266
267
		return render_template('login.html')
	session['user'] = ldapget(user)
	dbuser = query('SELECT * FROM users WHERE name = ?', user)
	if not dbuser:
Julian Rother's avatar
Julian Rother committed
268
		modify('INSERT INTO users (name, realname, fsacc, level, calendar_key, rfc6238) VALUES (?, ?, ?, 1, "", "")', user, session['user']['givenName'], user)
269
270
		dbuser = query('SELECT * FROM users WHERE name = ?', user)
	session['user']['dbid'] = dbuser[0]['id']
Julian Rother's avatar
Julian Rother committed
271
	return redirect(request.values.get('ref', url_for('index')))
Julian Rother's avatar
Julian Rother committed
272

Julian Rother's avatar
Julian Rother committed
273
@app.route('/logout', methods=['GET', 'POST'])
Julian Rother's avatar
Julian Rother committed
274
275
def logout():
	session.pop('user')
Julian Rother's avatar
Julian Rother committed
276
	return redirect(request.values.get('ref', url_for('index')))
Julian Rother's avatar
Julian Rother committed
277

278
# name: (tablename, idcolumn, [editable_fields], [fields_to_set_at_creation_time])
279
280
281
tabs = {
	'courses': ('courses_data', 'id', ['visible', 'listed', 'title', 'short',
			'handle', 'organizer', 'subject', 'semester', 'downloadable',
282
			'internal', 'responsible','deleted','description'],
283
			['created_by', 'time_created', 'time_updated']),
284
	'lectures': ('lectures_data', 'id', ['visible', 'title', 'comment',
285
286
287
288
289
290
291
292
293
294
			'internal', 'speaker', 'place', 'time', 'duration', 'jumplist','deleted'],
			['course_id', 'time_created', 'time_updated']),
	'videos': ('videos_data', 'id', ['visible','deleted'],
			['created_by', 'time_created', 'time_updated']),
	'chapters': ('chapters', 'id', ['time', 'text', 'visible', 'deleted'],
			['created_by', 'time_created', 'time_updated']),
	'announcements': ('announcements', 'id', ['text', 'level', 'visible',
			'deleted', 'time_publish', 'time_expire'],
			['created_by', 'time_created', 'time_updated']),
	'featured': ('featured', 'id', ['title', 'text', 'internal', 'visible', 'deleted'],
295
			['created_by', 'time_created', 'time_updated']),
296
	'auth': ('auth_data', 'auth_id', ['auth_type', 'auth_user', 'auth_passwd', 'deleted'],
297
298
299
			['course_id', 'lecture_id', 'video_id', 'created_by', 'time_created', 'time_updated']),
	'sorterrorlog': ('sorterrorlog_data', 'id', ['deleted'],
			['time_created', 'time_updated'])
300
301
}

302
@app.route('/edit', methods=['GET', 'POST'])
303
@mod_required
304
def edit(prefix='', ignore=[]):
305
	# All editable tables are expected to have a 'time_updated' field
306
	ignore.append('ref')
307
308
309
	ignore.append('prefix')
	if not prefix and 'prefix' in request.args:
		prefix = request.args['prefix']
Julian Rother's avatar
Julian Rother committed
310
	modify('BEGIN')
311
	changes = request.values.items()
312
	if request.is_json:
Julian Rother's avatar
Julian Rother committed
313
314
		changes = request.get_json().items()
	for key, val in changes:
315
316
317
		if key in ignore:
			continue
		key = prefix+key
318
		table, id, column = key.split('.', 2)
Julian Rother's avatar
Julian Rother committed
319
320
		assert table in tabs
		assert column in tabs[table][2]
321
322
		modify('INSERT INTO changelog (`table`,id_value, id_key, field, value_new, value_old, `when`, who, executed) VALUES (?,?,?,?,?,(SELECT %s FROM %s WHERE %s = ?),?,?,1)'%(column, tabs[table][0], tabs[table][1]),
				table, id, tabs[table][1], column, val, id, datetime.now(), session['user']['dbid'])
Julian Rother's avatar
Julian Rother committed
323
324
		modify('UPDATE %s SET %s = ?, time_updated = ? WHERE %s = ?'%(tabs[table][0], column, tabs[table][1]), val, datetime.now(), id)
	modify('COMMIT')
325
326
	if 'ref' in request.values:
		return redirect(request.values['ref'])
327
	return "OK", 200
Julian Rother's avatar
Julian Rother committed
328

329
@app.route('/new/<table>', methods=['GET', 'POST'])
330
@mod_required
331
332
def create(table):
	assert table in tabs
333
334
335
336
337
338
339
	defaults = {'created_by': session['user']['dbid'], 'time_created': datetime.now(), 'time_updated': datetime.now()}
	columns = []
	values = []
	for column, val in defaults.items():
		if column in tabs[table][3]:
			columns.append(column)
			values.append(val)
340
341
342
343
344
345
	args = request.values
	if request.is_json:
		args = request.get_json()
	for column, val in args.items():
		if column == 'ref':
			continue
346
347
		assert column in tabs[table][2]+tabs[table][3]
		assert column not in defaults
348
349
350
351
		columns.append(column)
		values.append(val)
	id = modify('INSERT INTO %s (%s) VALUES (%s)'%(tabs[table][0],
				','.join(columns), ','.join(['?']*len(values))), *values)
352
353
354
355
	if 'ref' in request.values:
		return redirect(request.values['ref'])
	return str(id), 200

356
357
358
359
360
@app.route('/auth')
def auth(): # For use with nginx auth_request
	if 'X-Original-Uri' not in request.headers:
		return 'Internal Server Error', 500
	url = request.headers['X-Original-Uri'].lstrip(config['VIDEOPREFIX'])
361
	ip = request.headers.get('X-Real-IP', '')
362
	if url.endswith('jpg'):
363
		return "OK", 200
364
	videos = query('''SELECT videos.path, videos.id, lectures.id AS lecture_id, courses.id AS course_id, auth.*
365
366
367
      FROM videos
      JOIN lectures ON (videos.lecture_id = lectures.id)
      JOIN courses ON (lectures.course_id = courses.id)
368
			LEFT JOIN auth ON (videos.id = auth.video_id OR lectures.id = auth.lecture_id OR courses.id = auth.course_id)
369
370
      WHERE videos.path = ?
      AND (? OR (courses.visible AND lectures.visible AND videos.visible))
371
			ORDER BY auth.video_id DESC, auth.lecture_id DESC, auth.course_id DESC''',
372
373
			url, ismod())
	if not videos:
374
		return "Not allowed", 403
375
376
377
378
	allowed = False
	types = []
	auth = request.authorization
	for video in videos:
379
380
		if videos[0] and ((videos[0]['video_id'] and not video['video_id']) \
				or (videos[0]['lecture_id'] and not video['lecture_id'])):
381
382
383
384
385
386
387
388
389
			break
		types.append(video['auth_type'])
		if video['auth_type'] == 'public':
			allowed = True
			break
		elif video['auth_type'] == 'password':
			if auth and video['auth_user'] == auth.username and video['auth_passwd'] == auth.password:
				allowed = True
				break
390
391
392
393
394
395
396
397
		elif video['auth_type'] == 'l2p':
			if video['auth_param'] in session.get('l2p_courses', []):
				allowed = True
				break
		elif video['auth_type'] == 'rwth':
			if session.get('rwthintern', False):
				allowed = True
				break
398
399
	if not types[0] or allowed or ismod() or \
			(auth and check_mod(*ldapauth(auth.username, auth.password))):
400
		return 'OK', 200
Julian Rother's avatar
Julian Rother committed
401
		modify('INSERT INTO log VALUES (?, "", ?, "video", ?, ?)', ip, datetime.now(), videos[0]['id'], url)
402
403
404
	elif 'password' in types:
		return Response("Login required", 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
	return "Not allowed", 403
Andreas Valder's avatar
Andreas Valder committed
405

Andreas Valder's avatar
Andreas Valder committed
406
@app.route('/stats')
Andreas Valder's avatar
Andreas Valder committed
407
@register_navbar('Statistiken', icon='stats')
Andreas Valder's avatar
Andreas Valder committed
408
409
410
@mod_required
def stats():
	return render_template('stats.html')
Andreas Valder's avatar
Andreas Valder committed
411

Andreas Valder's avatar
Andreas Valder committed
412
@app.route('/changelog')
Andreas Valder's avatar
Andreas Valder committed
413
@register_navbar('Changelog', icon='book')
Andreas Valder's avatar
Andreas Valder committed
414
@mod_required
415
def changelog():
416
417
418
	changelog = query('SELECT * FROM changelog LEFT JOIN users ON (changelog.who = users.id) ORDER BY `when` DESC LIMIT 50')
	for entry in changelog:
		entry['path'] = '.'.join([entry['table'], entry['id_value'], entry['field']])
419
	return render_template('changelog.html', changelog=changelog)
Andreas Valder's avatar
Andreas Valder committed
420

Julian Rother's avatar
Julian Rother committed
421
422
423
424
@app.route('/files/<filename>')
def files(filename):
	return redirect(config['VIDEOPREFIX']+'/'+filename)

425
426
427
428
429
@app.route('/newchapter/<int:lectureid>', methods=['POST', 'GET'])
def suggest_chapter(lectureid):
	time = request.values['time']
	text = request.values['text']
	assert(time and text)
430
431
432
433
434
435
436
	try:
		x = datetime.strptime(time,'%H:%M:%S')
		time= timedelta(hours=x.hour,minutes=x.minute,seconds=x.second).total_seconds()
		time = int(time)
	except ValueError:
		flash('Falsches Zeitformat, "%H:%M:%S" wird erwartet. Z.B. "01:39:42" für eine Kapitel bei Stunde 1, Minute 39, Sekunde 42')
		
437
438
439
	submitter = None
	if not ismod():
		submitter = request.environ['REMOTE_ADDR']
Julian Rother's avatar
Julian Rother committed
440
	id = modify('INSERT INTO chapters (lecture_id, time, text, time_created, time_updated, created_by, submitted_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
441
442
443
444
445
				lectureid, time, text, datetime.now(), datetime.now(), session.get('user', {'dbid':None})['dbid'], submitter)
	if 'ref' in request.values:
		return redirect(request.values['ref'])
	return 'OK',  200

446
447
448
449
450
451
452
453
454
455
@app.route('/chapters/<int:lectureid>')
def chapters(lectureid):
	chapters = query("SELECT * FROM chapters WHERE lecture_id = ? and visible ORDER BY time DESC", lectureid)
	last = None
	for c in chapters:
		c['start'] = c['time']
		c['end'] = last['start'] if last else 9999
		last = c
	return Response(render_template('chapters.srt',chapters=chapters), 200, {'Content-Type':'text/vtt'})

Andreas Valder's avatar
Andreas Valder committed
456
@app.route('/sitemap.xml')
Andreas Valder's avatar
Andreas Valder committed
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
def sitemap():
	pages=[]
	# static pages
	for rule in app.url_map.iter_rules():
		if 'GET' in rule.methods and len(rule.arguments)==0:
			if rule.endpoint not in mod_endpoints:
				pages.append([rule.rule])
	for i in query('select * from courses where visible and listed'):
		pages.append([url_for('course',handle=i['handle'])])
		for j in query('select * from lectures where (course_id = ? and visible)',i['id']):
			pages.append([url_for('lecture',id=j['id'])])


	return Response(render_template('sitemap.xml', pages=pages), 200, {'Content-Type': 'application/atom+xml'} )

Julian Rother's avatar
Julian Rother committed
472
import feeds
473
import importer
Andreas Valder's avatar
Andreas Valder committed
474
import timetable
Andreas Valder's avatar
Andreas Valder committed
475
import sorter
476
477
if 'ICAL_URL' in config:
	import meetings
478
479
if 'L2P_APIKEY' in config:
	import l2pauth