diff --git a/db.py b/db.py index db07d3397e35ddbcd677e3ea35d225d62f84bafa..55484b9e831550c75331bff91e1445236505ea46 100644 --- a/db.py +++ b/db.py @@ -1,6 +1,7 @@ from server import * import sqlite3 import re +import datetime if config['DB_ENGINE'] == 'sqlite': created = not os.path.exists(config['SQLITE_DB']) @@ -23,6 +24,25 @@ def dict_factory(cursor, row): d[col[0].split('.')[-1]] = row[idx] return d +# From sqlite3 module, but with error catching +def sqlite_convert_timestamp(val): + try: + datepart, timepart = val.split(b" ") + year, month, day = map(int, datepart.split(b"-")) + timepart_full = timepart.split(b".") + hours, minutes, seconds = map(int, timepart_full[0].split(b":")) + if len(timepart_full) == 2: + microseconds = int('{:0<6.6}'.format(timepart_full[1].decode())) + else: + microseconds = 0 + val = datetime.datetime(year, month, day, hours, minutes, seconds, microseconds) + except: + val = None + return val + +sqlite3.register_converter('datetime', sqlite_convert_timestamp) +sqlite3.register_converter('timestamp', sqlite_convert_timestamp) + def query(operation, *params): if config['DB_ENGINE'] == 'mysql': import mysql.connector @@ -33,7 +53,7 @@ def query(operation, *params): request.db.execute(operation.replace('?', '%s'), params) elif config['DB_ENGINE'] == 'sqlite': if 'db' not in g: - g.db = sqlite3.connect(config['SQLITE_DB']) + g.db = sqlite3.connect(config['SQLITE_DB'], detect_types=sqlite3.PARSE_DECLTYPES) g.db.row_factory = dict_factory g.db.isolation_level = None if not hasattr(request, 'db'): @@ -99,6 +119,8 @@ def ldapget(user): conn = ldap3.Connection('ldaps://rumo.fsmpi.rwth-aachen.de', auto_bind=True) conn.search("ou=users,dc=fsmpi,dc=rwth-aachen,dc=de", "(uid=%s)"%user, attributes=ldap3.ALL_ATTRIBUTES) + if not conn.entries: + return {} e = conn.entries[0] return {'uid': user, 'givenName': e.givenName.value, 'sn':e.sn.value} else: diff --git a/server.py b/server.py index 24597db5786d7f9ce0822bca6308d8ae6b78a47b..0bb894525274fbe54439cec74a7131c0a07794de 100755 --- a/server.py +++ b/server.py @@ -1,11 +1,23 @@ #!/bin/python from flask import Flask, g, request, url_for, redirect, session, render_template, flash +from werkzeug.routing import Rule from functools import wraps from datetime import date, timedelta, datetime, time +import threading import os app = Flask(__name__) +def timer_func(): + with app.test_request_context(): + pass # do something + timer = threading.Timer(60*60, timer_func) + timer.start() + +timer = threading.Timer(0, timer_func) +timer.daemon = True +timer.start() + config = app.config config['DB_SCHEMA'] = 'db_schema.sql' config['DB_DATA'] = 'db_example.sql' @@ -22,14 +34,12 @@ config.from_pyfile('config.py', silent=True) from db import query, searchquery, ldapauth, ldapget -app.jinja_env.globals['videoprefix'] = config['VIDEOPREFIX'] mod_endpoints = [] +@app.template_global() def ismod(*args): return ('user' in session) -app.jinja_env.globals['ismod'] = ismod - def mod_required(func): mod_endpoints.append(func.__name__) @wraps(func) @@ -50,6 +60,28 @@ def register_navbar(name, icon=None): return func return wrapper +def render_endpoint(endpoint, flashtext=None, **kargs): + if flashtext: + flash(flashtext) + # request.endpoint is used for navbar highlighting + request.url_rule = Rule(request.path, endpoint=endpoint) + return app.view_functions[endpoint](**kargs) + +def handle_errors(endpoint, text, code, *errors, **epargs): + def wrapper(func): + @wraps(func) + def decorator(*args, **kwargs): + try: + return func(*args, **kwargs) + except errors: + return render_endpoint(endpoint, text, **epargs), code + return decorator + return wrapper + +@app.errorhandler(404) +def handle_not_found(e): + return render_endpoint('index', 'Diese Seite existiert nicht!'), 404 + @app.route('/') @register_navbar('Home', icon='home') def index(): @@ -72,16 +104,18 @@ def course(): if course['semester'] == '': course['semester'] = 'zeitlos' groupedby = request.args.get('groupedby') - if groupedby not in ['title','semester','organizer']: + if groupedby not in ['title', 'semester', 'organizer']: groupedby = 'semester' return render_template('course.html', courses=courses, groupedby=groupedby) @app.route('/course/<id>') -def course_id(id): - courses = query('SELECT * FROM courses WHERE ((handle = ?) or id = ?) AND (? OR visible)', id, id, ismod()) - if not courses: - flash('Diese Veranstaltung existiert nicht!') - return app.view_functions['course'](), 404 +@app.route('/course/<int:numid>') +@handle_errors('course', 'Diese Veranstaltung existiert nicht!', 404, IndexError) +def course_id(numid=None, id=None): + if numid: + courses = query('SELECT * FROM courses WHERE id = ? AND (? OR visible)', numid, ismod()) + else: + courses = query('SELECT * FROM courses WHERE handle = ? AND (? OR visible)', id, ismod()) lectures = query('SELECT * FROM lectures WHERE course_id = ? AND (? OR visible)', courses[0]['id'], ismod()) videos = query(''' SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, formats.description AS format_description @@ -100,18 +134,15 @@ def faq(): return render_template('faq.html') @app.route('/play/<int:id>') +@handle_errors('course', 'Diese Vorlesung existiert nicht!', 404, IndexError) def play(id): lectures = query('SELECT * FROM lectures WHERE id = ? AND (? OR visible)', id, ismod()) videos = query('SELECT * FROM videos WHERE lecture_id = ? AND (? OR visible)', id, ismod()) - if not lectures: - flash('Diese Vorlesung existiert nicht!') - return app.view_functions['course'](), 404 if not videos: flash('Zu dieser Vorlesung wurden noch keine Videos veröffentlicht!') courses = query('SELECT * FROM courses WHERE id = ? AND (? OR (visible AND listed))', lectures[0]['course_id'], ismod()) if not courses: - flash('Diese Veranstaltung existiert nicht!') - return app.view_functions['course'](), 404 + return render_endpoint('course', 'Diese Veranstaltung existiert nicht!'), 404 return render_template('play.html', course=courses[0], lecture=lectures[0], videos=videos) @app.route('/search') @@ -132,10 +163,15 @@ def login(): if request.method == 'GET': return render_template('login.html') user, groups = ldapauth(request.form.get('user'), request.form.get('password')) - if user and 'users' in groups: - session['user'] = ldapget(user) - else: + if not user or not 'users' in groups: flash('Login fehlgeschlagen!') + return render_template('login.html') + session['user'] = ldapget(user) + dbuser = query('SELECT * FROM users WHERE name = ?', user) + if not dbuser: + query('INSERT INTO users (name, realname, fsacc, level, calendar_key, rfc6238) VALUES (?, ?, ?, 1, "", "")', user, session['user']['givenName'], user) + dbuser = query('SELECT * FROM users WHERE name = ?', user) + session['user']['dbid'] = dbuser[0]['id'] return redirect(request.values.get('ref', url_for('index'))) @app.route('/logout', methods=['GET', 'POST']) @@ -165,8 +201,7 @@ def edit(): assert table in tabs assert column in tabs[table][2] query('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']['givenName']) - query('UPDATE %s SET %s = ? WHERE %s = ?'%(tabs[table][0], column, - tabs[table][1]), val, id) + query('UPDATE %s SET %s = ? WHERE %s = ?'%(tabs[table][0], column,tabs[table][1]), val, id) query('COMMIT') return "OK", 200 @@ -224,10 +259,9 @@ def schedule(): curcol=0; freecol=[]; for l in i['lectures']: - l['time_asdate'] = datetime.strptime(l['time'],'%Y-%m-%d %H:%M:%S') # who the hell inserts lectures with zero length?!?!? - l['end_asdate'] = l['time_asdate']+timedelta(minutes=max(l['duration'],1)) - for l in sorted([(l['time_asdate'],True,l) for l in i['lectures']] + [(l['end_asdate'],False,l) for l in i['lectures']],key=lambda t:(t[0],t[1])): + l['time_end'] = l['time']+timedelta(minutes=max(l['duration'],1)) + for l in sorted([(l['time'],True,l) for l in i['lectures']] + [(l['time_end'],False,l) for l in i['lectures']],key=lambda t:(t[0],t[1])): if l[1]: curcol += 1 if curcol > maxcol: @@ -262,4 +296,5 @@ def stats(): @register_navbar('Changelog', 'book') @mod_required def log(): - return render_template('log.html', changelog=query('SELECT *, ( "table" || "." || id_value || "." ||field) as path FROM changelog ORDER BY "when" DESC LIMIT 50')) + changelog = query('SELECT *, ( "table" || "." || id_value || "." ||field) as path FROM changelog LEFT JOIN users ON (changelog.who = users.id) ORDER BY "when" DESC LIMIT 50') + return render_template('log.html', changelog=changelog) diff --git a/templates/base.html b/templates/base.html index 7996514e0afcbfd0f6706d8373100a31c591be44..726ec229268b803ca2aea1f15fc631b9f65d0d7e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -49,7 +49,7 @@ {% for endpoint, caption, gly, visible in navbar %} {% if visible or ismod() %} <li{% if endpoint == request.endpoint %} class="active"{% endif %}> - <a href="{{ url_for(endpoint)|e }}">{% if gly != '' %}<span class="glyphicon glyphicon-{{ gly }}"></span> {% endif %}{{ caption }}</a> + <a href="{{ url_for(endpoint) }}">{% if gly != '' %}<span class="glyphicon glyphicon-{{ gly }}"></span> {% endif %}{{ caption }}</a> </li> {% endif %} {% endfor %} @@ -63,12 +63,12 @@ { html:true, title:'Login für Moderatoren', - content:'<form method="post" action="{{url_for('login')}}"><input placeholder="User" name="user" type="text"><br><input placeholder="Password" name="password" type="password"><br><input type="hidden" name="ref" value="{{ request.url|e }}"><input type="submit" value="Login"></form>' + content:'<form method="post" action="{{url_for('login', ref=request.url)}}"><input placeholder="User" name="user" type="text"><br><input placeholder="Password" name="password" type="password"><br><input type="submit" value="Login"></form>' } ) </script> {% else %} - <a href="{{url_for('logout')}}?ref={{ request.url|urlencode }}"> + <a href="{{url_for('logout', ref=request.url)}}"> {{ session.user.givenName }} <span class="glyphicon glyphicon-log-out"></span> </a> diff --git a/templates/log.html b/templates/log.html index d67e0033fe1024f1806c81dd90b41e4754b9aa72..a3970c2a2205c1c583eff2344421a4b1854d3140 100644 --- a/templates/log.html +++ b/templates/log.html @@ -19,7 +19,11 @@ {% for i in changelog %} <tr> <td>{{i.when}}</td> - <td>{{i.who}}</td> + {% if i.realname %} + <td>{{i.realname}} ({{i.who}})</td> + {% else %} + <td>{{i.who}}</td> + {% endif %} <td>{{i.path}}</td> <td>"{{i.value_old}}"</td> <td>"{{i.value_new}}"</td> diff --git a/templates/macros.html b/templates/macros.html index dac9b903b24a6ae24f301b3f873ef11fb25b357c..2f205d05e6b82c034a4599fa33793c5100b4574f 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -2,7 +2,7 @@ <li class="list-group-item"> <a class="hidden-xs" href="{{url_for('play', id=lecture['id'])}}" title="{{ lecture['coursetitle'] }}"> <div class="row"> - <img class="col-xs-4" src="{{ videoprefix }}/{{ lecture['titlefile'] }}" alt="Vorschaubild"> + <img class="col-xs-4" src="{{ config.VIDEOPREFIX }}/{{ lecture['titlefile'] }}" alt="Vorschaubild"> <div class="col-xs-4"> <span style="color: #000;"><strong>{{ lecture['short'] }}</strong></span><br> <span style="color: #000;">{{ lecture['time'] }}</span> @@ -18,7 +18,7 @@ </a> <a class="visible-xs" href="{{url_for('play', id=lecture['id'])}}" title="{{ lecture['coursetitle'] }}"> <div class="row"> - <img class="col-xs-12" src="{{ videoprefix }}/{{ lecture['titlefile'] }}" alt="Vorschaubild"> + <img class="col-xs-12" src="{{ config.VIDEOPREFIX }}/{{ lecture['titlefile'] }}" alt="Vorschaubild"> </div> <div class="row"> <div class="col-xs-12"> @@ -53,7 +53,7 @@ <link rel="stylesheet" href="{{url_for('static', filename='mediaelementjs/mediaelementplayer.css')}}" /> <video class="mejs-player" width="640" height="360" style="width: 100%; height: 100%;"> {% for v in videos %} - <source type="video/mp4" src="{{ videoprefix }}/{{ v.path }}" /> + <source type="video/mp4" src="{{ config.VIDEOPREFIX }}/{{ v.path }}" /> {% endfor %} </video> <script> @@ -67,7 +67,7 @@ {% macro course_list_item(course,show_semester=False) %} <li class="list-group-item {% if (not course.visible) or (not course.listed) %}list-group-item-danger{% endif %}"> <div class="row"> - <a href="{{url_for('course_id', id=course.handle)}}"> + <a href="{{url_for('course_id', numid=course.id)}}"> {% if show_semester %} <span class="col-xs-1"> {{ course.semester }} @@ -93,13 +93,13 @@ <button class="btn btn-primary dropdown-toggle {% if videos|length is equalto 0 %}disabled{% endif %}" type="button" data-toggle="dropdown">Download <span class="caret"></span></button> <ul class="dropdown-menu"> {% for v in videos %} - {% if v.downloadable %} <li><a href="{{ videoprefix }}/{{v.path}}">{{ valuecheckbox(['videos',v.id,'visible'], v.visible) }} {{v.format_description}} ({{v.file_size|filesizeformat(true)}})</a></li>{% endif %} + {% if v.downloadable %} <li><a href="{{ config.VIDEOPREFIX }}/{{v.path}}">{{ valuecheckbox(['videos',v.id,'visible'], v.visible) }} {{v.format_description}} ({{v.file_size|filesizeformat(true)}})</a></li>{% endif %} {% endfor %} </ul> <noscript> <ul class="pull-right list-unstyled" style="margin-left:10px;"> {% for v in videos %} - <li><a href="{{ videoprefix }}/{{v.path}}">{{v.format_description}} ({{v.file_size|filesizeformat(true)}})</a></li> + <li><a href="{{ config.VIDEOPREFIX }}/{{v.path}}">{{v.format_description}} ({{v.file_size|filesizeformat(true)}})</a></li> {% endfor %} </ul> </noscript> diff --git a/templates/play.html b/templates/play.html index 6ffa50688b04ebf0ed70bf4a9622290f538132af..59a9bc11bf4014371f3fb762c0b9debdc0ef2b45 100644 --- a/templates/play.html +++ b/templates/play.html @@ -11,7 +11,7 @@ </div> <div class="panel-body"> <p class="col-xs-12" style="padding: 0px;"> - <span><a href="{{url_for('course_id', id=course.handle)}}" class="btn btn-default" >Zur Veranstaltungsseite</a><span> + <span><a href="{{url_for('course_id', numid=course.id)}}#lecture-{{lecture.id}}" class="btn btn-default" >Zur Veranstaltungsseite</a><span> <span class="pull-right"> <span>{{ video_embed_btn(lecture.id) }}</span> <span>{{ video_download_btn(videos) }}</span> diff --git a/templates/schedule.html b/templates/schedule.html index dbd4123463705f73214091cc3f330893b92bb92c..8a6a883b4a1af38e966e617bbc5cf16e7ad2d928 100644 --- a/templates/schedule.html +++ b/templates/schedule.html @@ -21,15 +21,15 @@ {% if ((loop.index - 1) is divisibleby 4) %} <td rowspan="4" style="vertical-align: top;">{{ t.strftime("%H:%M") }}</td> {% endif %} {% for d in days if (d.index < 5) or (d.lectures|length) > 0 %} {% for i in range(1,d.maxcol+1) %} - {% for l in d.lectures|selectattr('schedule_col','equalto',i) if (((l.time_asdate.time() > t) and (l.time_asdate.time() < times[time_loop.index+1])) != (l.time_asdate.time() == t ) ) %} + {% for l in d.lectures|selectattr('schedule_col','equalto',i) if (((l.time.time() > t) and (l.time.time() < times[time_loop.index+1])) != (l.time.time() == t ) ) %} <td rowspan="{{l.duration / 15}}" style="background: lightgrey;"> <p class="small"> - <strong><a href="{{url_for('course_id', id=l['course_id'])}}#lecture-{{l.id}}">{{l.short}}</a></strong><br> - {{l.time_asdate.strftime("%H:%M")}} - {{l.end_asdate.strftime("%H:%M")}}<br> + <strong><a href="{{url_for('course_id', numid=l['course_id'])}}#lecture-{{l.id}}">{{l.short}}</a></strong><br> + {{l.time.strftime("%H:%M")}} - {{l.time_end.strftime("%H:%M")}}<br> {{l.place}}</p> </td> {% else %} - {% for l in d.lectures|selectattr('schedule_col','equalto',i) if (l.time_asdate.time() < t) and (l.end_asdate.time() > t) %} + {% for l in d.lectures|selectattr('schedule_col','equalto',i) if (l.time.time() < t) and (l.time_end.time() > t) %} {% else %} <td></td> {% endfor %}