Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • bootstrap4
  • intros
  • master
  • modules
  • postgres_integration
  • s3compatible
6 results

Target

Select target project
  • jannik/website
  • vincent/website
  • dominic/website
  • romank/website
  • videoaginfra/website
5 results
Select Git revision
  • bootstrap4
  • forbid-save-as
  • intros
  • live_sources
  • master
  • modules
  • moodle-integration
  • patch-double-tap-seek
  • patch_datum_anzeigen
  • patch_raum_anzeigen
  • upload-via-token
11 results
Show changes
Showing
with 871 additions and 420 deletions
......@@ -58,11 +58,11 @@
this.controlText('Quality');
if(options.dynamicLabel){
videojs.addClass(this.label, 'vjs-resolution-button-label');
videojs.dom.addClass(this.label, 'vjs-resolution-button-label');
this.el().appendChild(this.label);
}else{
var staticLabel = document.createElement('span');
videojs.addClass(staticLabel, 'vjs-menu-icon');
videojs.dom.addClass(staticLabel, 'vjs-menu-icon');
this.el().appendChild(staticLabel);
}
player.on('updateSources', videojs.bind( this, this.update ) );
......@@ -214,10 +214,8 @@
.setSourcesSanitized(sources, label, customSourcePicker || settings.customSourcePicker)
.one(handleSeekEvent, function() {
player.currentTime(currentTime);
player.handleTechSeeked_();
if(!isPaused){
// Start playing and hide loadingSpinner (flash issue ?)
player.play().handleTechSeeked_();
player.play();
}
player.trigger('resolutionchange');
});
......@@ -401,6 +399,6 @@
};
// register the plugin
videojs.plugin('videoJsResolutionSwitcher', videoJsResolutionSwitcher);
videojs.registerPlugin('videoJsResolutionSwitcher', videoJsResolutionSwitcher);
})(window, videojs);
})();
......@@ -7,17 +7,20 @@
*/
;(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory.bind(this, root, root.videojs));
if (typeof window !== 'undefined' && window.videojs) {
factory(window.videojs);
} else if (typeof define === 'function' && define.amd) {
define('videojs-hotkeys', ['video.js'], function (module) {
return factory(module.default || module);
});
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = factory(root, root.videojs);
} else {
factory(root, root.videojs);
module.exports = factory(require('video.js'));
}
})(window, function(window, videojs) {
}(this, function (videojs) {
"use strict";
window['videojs_hotkeys'] = { version: "0.2.16" };
if (typeof window !== 'undefined') {
window['videojs_hotkeys'] = { version: "0.2.25" };
}
var hotkeys = function(options) {
var player = this;
......@@ -28,11 +31,14 @@
seekStep: 5,
enableMute: true,
enableVolumeScroll: true,
enableHoverScroll: false,
enableFullscreen: true,
enableNumbers: true,
enableJogStyle: false,
alwaysCaptureHotkeys: false,
enableModifiersForNumbers: true,
enableInactiveFocus: true,
skipInitialFocus: false,
playPauseKey: playPauseKey,
rewindKey: rewindKey,
forwardKey: forwardKey,
......@@ -59,11 +65,16 @@
seekStep = options.seekStep,
enableMute = options.enableMute,
enableVolumeScroll = options.enableVolumeScroll,
enableHoverScroll = options.enableHoverScroll,
enableFull = options.enableFullscreen,
enableNumbers = options.enableNumbers,
enableJogStyle = options.enableJogStyle,
alwaysCaptureHotkeys = options.alwaysCaptureHotkeys,
enableModifiersForNumbers = options.enableModifiersForNumbers;
enableModifiersForNumbers = options.enableModifiersForNumbers,
enableInactiveFocus = options.enableInactiveFocus,
skipInitialFocus = options.skipInitialFocus;
var videojsVer = videojs.VERSION;
// Set default player tabindex to handle keydown and doubleclick events
if (!pEl.hasAttribute('tabIndex')) {
......@@ -74,11 +85,14 @@
pEl.style.outline = "none";
if (alwaysCaptureHotkeys || !player.autoplay()) {
if (!skipInitialFocus) {
player.one('play', function() {
pEl.focus(); // Fixes the .vjs-big-play-button handing focus back to body instead of the player
});
}
}
if (enableInactiveFocus) {
player.on('userinactive', function() {
// When the control bar fades, re-apply focus to the player if last focus was a control button
var cancelFocusingPlayer = function() {
......@@ -86,13 +100,16 @@
};
var focusingPlayerTimeout = setTimeout(function() {
player.off('useractive', cancelFocusingPlayer);
if (doc.activeElement.parentElement == pEl.querySelector('.vjs-control-bar')) {
var activeElement = doc.activeElement;
var controlBar = pEl.querySelector('.vjs-control-bar');
if (activeElement && activeElement.parentElement == controlBar) {
pEl.focus();
}
}, 10);
player.one('useractive', cancelFocusingPlayer);
});
}
player.on('play', function() {
// Fix allowing the YouTube plugin to have hotkey support.
......@@ -104,8 +121,9 @@
});
var keyDown = function keyDown(event) {
var ewhich = event.which, curTime;
var ewhich = event.which, wasPlaying, seekTime;
var ePreventDefault = event.preventDefault;
var duration = player.duration();
// When controls are disabled, hotkeys will be disabled as well
if (player.controls()) {
......@@ -127,7 +145,7 @@
}
if (player.paused()) {
player.play();
silencePromise(player.play());
} else {
player.pause();
}
......@@ -135,18 +153,38 @@
// Seeking with the left/right arrow keys
case cRewind: // Seek Backward
wasPlaying = !player.paused();
ePreventDefault();
curTime = player.currentTime() - seekStep;
if (wasPlaying) {
player.pause();
}
seekTime = player.currentTime() - seekStepD(event);
// The flash player tech will allow you to seek into negative
// numbers and break the seekbar, so try to prevent that.
if (player.currentTime() <= seekStep) {
curTime = 0;
if (seekTime <= 0) {
seekTime = 0;
}
player.currentTime(seekTime);
if (wasPlaying) {
silencePromise(player.play());
}
player.currentTime(curTime);
break;
case cForward: // Seek Forward
wasPlaying = !player.paused();
ePreventDefault();
player.currentTime(player.currentTime() + seekStep);
if (wasPlaying) {
player.pause();
}
seekTime = player.currentTime() + seekStepD(event);
// Fixes the player not sending the end event if you
// try to seek past the duration on the seekbar.
if (seekTime >= duration) {
seekTime = wasPlaying ? duration - .001 : duration;
}
player.currentTime(seekTime);
if (wasPlaying) {
silencePromise(player.play());
}
break;
// Volume control with the up/down arrow keys
......@@ -155,11 +193,11 @@
if (!enableJogStyle) {
player.volume(player.volume() - volumeStep);
} else {
curTime = player.currentTime() - 1;
seekTime = player.currentTime() - 1;
if (player.currentTime() <= 1) {
curTime = 0;
seekTime = 0;
}
player.currentTime(curTime);
player.currentTime(seekTime);
}
break;
case cVolumeUp:
......@@ -167,7 +205,11 @@
if (!enableJogStyle) {
player.volume(player.volume() + volumeStep);
} else {
player.currentTime(player.currentTime() + 1);
seekTime = player.currentTime() + 1;
if (seekTime >= duration) {
seekTime = duration;
}
player.currentTime(seekTime);
}
break;
......@@ -214,7 +256,7 @@
// Check if the custom key's condition matches
if (customHotkey.key(event)) {
ePreventDefault();
customHotkey.handler(player, options);
customHotkey.handler(player, options, event);
}
}
}
......@@ -224,6 +266,8 @@
};
var doubleClick = function doubleClick(event) {
// Video.js added double-click fullscreen in 7.1.0
if (videojsVer != null && videojsVer <= "7.1.0") {
// When controls are disabled, hotkeys will be disabled as well
if (player.controls()) {
......@@ -242,17 +286,32 @@
}
}
}
}
};
var volumeHover = false;
var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel');
if (volumeSelector != null) {
volumeSelector.onmouseover = function() { volumeHover = true; };
volumeSelector.onmouseout = function() { volumeHover = false; };
}
var mouseScroll = function mouseScroll(event) {
if (enableHoverScroll) {
// If we leave this undefined then it can match non-existent elements below
var activeEl = 0;
} else {
var activeEl = doc.activeElement;
}
// When controls are disabled, hotkeys will be disabled as well
if (player.controls()) {
var activeEl = event.relatedTarget || event.toElement || doc.activeElement;
if (alwaysCaptureHotkeys ||
activeEl == pEl ||
activeEl == pEl.querySelector('.vjs-tech') ||
activeEl == pEl.querySelector('.iframeblocker') ||
activeEl == pEl.querySelector('.vjs-control-bar')) {
activeEl == pEl.querySelector('.vjs-control-bar') ||
volumeHover) {
if (enableVolumeScroll) {
event = window.event || event;
......@@ -343,6 +402,17 @@
return (e.which === 70);
}
function seekStepD(e) {
// SeekStep caller, returns an int, or a function returning an int
return (typeof seekStep === "function" ? seekStep(e) : seekStep);
}
function silencePromise(value) {
if (value != null && typeof value.then === 'function') {
value.then(null, function(e) {});
}
}
player.on('keydown', keyDown);
player.on('dblclick', doubleClick);
player.on('mousewheel', mouseScroll);
......@@ -351,5 +421,6 @@
return this;
};
videojs.plugin('hotkeys', hotkeys);
});
var registerPlugin = videojs.registerPlugin || videojs.plugin;
registerPlugin('hotkeys', hotkeys);
}));
from server import *
import json
from hashlib import md5
from datetime import datetime
from server import *
@app.route('/internal/stats')
@app.route('/internal/stats/<semester>')
@register_navbar('Statistiken', icon='stats')
@mod_required
def stats():
semester = query('SELECT DISTINCT semester from courses WHERE semester != ""');
for s in semester:
year = int(s['semester'][0:4])
if s['semester'].endswith('ss'):
s['from'] = datetime(year, 4, 1)
s['to'] = datetime(year, 10, 1)
if s['semester'].endswith('ws'):
s['from'] = datetime(year, 10, 1)
s['to'] = datetime(year+1, 4, 1)
semester = query('SELECT DISTINCT semester from courses WHERE semester != \'\'')
for i in semester:
year = int(i['semester'][0:4])
if i['semester'].endswith('ss'):
i['from'] = datetime(year, 4, 1)
i['to'] = datetime(year, 10, 1)
if i['semester'].endswith('ws'):
i['from'] = datetime(year, 10, 1)
i['to'] = datetime(year+1, 4, 1)
return render_template('stats.html', semester=semester, filter=request.args.get('filter'))
statsqueries = {}
statsqueries['formats_views'] = "SELECT formats.description AS labels, count(DISTINCT log.id) AS `values` FROM log JOIN videos ON (videos.id = log.video) JOIN formats ON (formats.id = videos.video_format) GROUP BY formats.id"
statsqueries['course_count'] = 'SELECT semester AS x, count(id) AS y FROM courses WHERE semester != "" GROUP BY semester'
statsqueries['lectures_count'] = 'SELECT semester AS x, count(lectures.id) AS y FROM lectures JOIN courses ON (courses.id = lectures.course_id) WHERE semester != "" GROUP BY semester'
statsqueries['categories_courses'] = "SELECT courses.subject AS labels, count(courses.id) AS `values` FROM courses GROUP BY courses.subject ORDER BY labels DESC LIMIT 100"
statsqueries['organizer_courses'] = "SELECT courses.organizer AS labels, count(courses.id) AS `values` FROM courses GROUP BY courses.organizer ORDER BY labels DESC LIMIT 100"
statsqueries['categories_lectures'] = "SELECT courses.subject AS labels, count(lectures.id) AS `values` FROM lectures JOIN courses ON (courses.id = lectures.course_id) WHERE lectures.visible GROUP BY courses.subject ORDER BY `values` DESC LIMIT 100"
statsqueries['lecture_views'] = "SELECT lectures.time AS x, count(DISTINCT log.id) AS y FROM log JOIN videos ON (videos.id = log.video) JOIN lectures ON (lectures.id = videos.lecture_id) WHERE (lectures.course_id = ?) GROUP BY lectures.id ORDER BY lectures.time"
statsqueries['live_views'] = "SELECT hlslog.segment AS x, COUNT(DISTINCT hlslog.id) AS y FROM hlslog WHERE hlslog.lecture = ? GROUP BY hlslog.segment ORDER BY hlslog.segment"
statsqueries['lecture_totalviews'] = "SELECT 42"
STATS_QUERIES = {}
STATS_QUERIES['formats_views'] = "SELECT formats.description AS labels, count(DISTINCT log.id) AS \"values\" FROM log \
JOIN videos ON (videos.id = log.video) JOIN formats ON (formats.id = videos.video_format) GROUP BY formats.id, formats.description"
STATS_QUERIES['course_count'] = 'SELECT semester AS x, count(id) AS y FROM courses WHERE semester != \'\' GROUP BY semester ORDER BY semester ASC'
STATS_QUERIES['lectures_count'] = 'SELECT semester AS x, count(lectures.id) AS y FROM lectures \
JOIN courses ON (courses.id = lectures.course_id) WHERE semester != \'\' GROUP BY semester ORDER BY semester ASC'
STATS_QUERIES['categories_courses'] = "SELECT courses.subject AS labels, count(courses.id) AS \"values\" FROM courses \
GROUP BY courses.subject ORDER BY labels DESC LIMIT 100"
STATS_QUERIES['organizer_courses'] = "SELECT courses.organizer AS labels, count(courses.id) AS \"values\" FROM courses \
GROUP BY courses.organizer ORDER BY labels DESC LIMIT 100"
STATS_QUERIES['categories_lectures'] = "SELECT courses.subject AS labels, count(lectures.id) AS \"values\" FROM lectures \
JOIN courses ON (courses.id = lectures.course_id) WHERE lectures.visible GROUP BY courses.subject ORDER BY \"values\" DESC LIMIT 100"
STATS_QUERIES['lecture_views'] = "SELECT lectures.time AS x, count(DISTINCT log.id) AS y FROM log \
JOIN videos ON (videos.id = log.video) \
JOIN lectures ON (lectures.id = videos.lecture_id) \
WHERE (lectures.course_id = ?) GROUP BY lectures.id, lectures.time ORDER BY lectures.time"
STATS_QUERIES['live_views'] = "SELECT hlslog.segment AS x, COUNT(DISTINCT hlslog.id) AS y FROM hlslog WHERE hlslog.lecture = ? \
GROUP BY hlslog.segment ORDER BY hlslog.segment"
STATS_QUERIES['lecture_totalviews'] = "SELECT 42"
def plotly_date_handler(obj):
return obj.strftime("%Y-%m-%d %H:%M:%S")
......@@ -37,9 +46,9 @@ def plotly_date_handler(obj):
@app.route('/internal/stats/generic/<req>/<param>')
@mod_required
def stats_generic(req, param=None):
if req not in statsqueries:
if req not in STATS_QUERIES:
return 404, 'Not found'
rows = query(statsqueries[req], *(statsqueries[req].count('?')*[param]))
rows = query(STATS_QUERIES[req], *(STATS_QUERIES[req].count('?')*[param]))
if req == 'live_views':
res = {'x': [], 'y': []}
else:
......@@ -61,10 +70,10 @@ def stats_generic(req, param=None):
@app.route('/internal/stats/viewsperday/<req>')
@app.route('/internal/stats/viewsperday/<req>/<param>')
@mod_required
def stats_viewsperday(req, param=""):
update_expr = 'INSERT INTO logcache (req, param, trace, date, value) SELECT "%s", ?, trace, date, y FROM (%s) AS cachetmp WHERE date < ?'
query_expr = 'SELECT date, trace, value AS y FROM logcache WHERE req = "%s" AND param = ? UNION SELECT * FROM (%s) AS cachetmp'
date_subexpr = 'SELECT CASE WHEN MAX(date) IS NULL THEN "2000-00-00" ELSE MAX(date) END AS t FROM `logcache` WHERE req = "%s" AND param = ?'
def stats_viewsperday(req, param=""): #pylint: disable=too-many-locals
update_expr = 'INSERT INTO logcache (req, param, trace, date, value) SELECT \'%s\', ?, trace, date, y FROM (%s) AS cachetmp WHERE date < ?'
query_expr = 'SELECT date, trace, value AS y FROM logcache WHERE req = \'%s\' AND param = ? UNION SELECT * FROM (%s) AS cachetmp'
date_subexpr = 'SELECT CASE WHEN MAX(date) IS NULL THEN \'2000-01-01\' ELSE MAX(date) END AS t FROM "logcache" WHERE req = \'%s\' AND param = ?'
queries = {
'lecture': # views per day per lecture (split per format)
'''SELECT log.date AS date, formats.description AS trace, COUNT(DISTINCT log.id) AS y
......@@ -72,9 +81,9 @@ def stats_viewsperday(req, param=""):
JOIN videos ON videos.id = log.video
JOIN formats ON formats.id = videos.video_format
WHERE log.date > %T AND videos.lecture_id = ?
GROUP BY log.date, videos.video_format
GROUP BY log.date, formats.description, videos.video_format
UNION
SELECT log.date AS date, "total" AS trace, COUNT(DISTINCT log.id) AS y
SELECT log.date AS date, \'total\' AS trace, COUNT(DISTINCT log.id) AS y
FROM log JOIN videos ON videos.id = log.video
WHERE log.date > %T AND videos.lecture_id = ? GROUP BY log.date''',
......@@ -84,9 +93,9 @@ def stats_viewsperday(req, param=""):
JOIN lectures ON lectures.id = videos.lecture_id
JOIN formats ON formats.id = videos.video_format
WHERE log.date > %T AND lectures.course_id = ?
GROUP BY log.date, videos.video_format
GROUP BY log.date, formats.description, videos.video_format
UNION
SELECT log.date AS date, "total" AS trace, COUNT(DISTINCT log.id) AS y
SELECT log.date AS date, \'total\' AS trace, COUNT(DISTINCT log.id) AS y
FROM log
JOIN videos ON videos.id = log.video
JOIN lectures ON lectures.id = videos.lecture_id
......@@ -98,9 +107,9 @@ def stats_viewsperday(req, param=""):
FROM log
JOIN videos ON videos.id = log.video
JOIN formats ON formats.id = videos.video_format
WHERE log.date > %T GROUP BY log.date, videos.video_format
WHERE log.date > %T GROUP BY log.date, formats.description, videos.video_format
UNION
SELECT log.date AS date, "total" AS trace, COUNT(DISTINCT log.id) AS y
SELECT log.date AS date, \'total\' AS trace, COUNT(DISTINCT log.id) AS y
FROM log
WHERE log.date > %T
GROUP BY log.date''',
......@@ -111,27 +120,31 @@ def stats_viewsperday(req, param=""):
JOIN lectures ON lectures.id = videos.lecture_id
JOIN courses ON courses.id = lectures.course_id
WHERE log.date > %T
GROUP BY log.date, courses.id'''
GROUP BY log.date, courses.handle, courses.id'''
}
expr = queries[req].replace('%T', '"'+query(date_subexpr%('viewsperday.'+req), param)[0]['t']+'"')
expr = queries[req].replace('%T', '\''+str(query(date_subexpr%('viewsperday.'+req), param)[0]['t'])+'\'')
params = [param]*expr.count('?')
try:
modify("BEGIN")
modify(update_expr%('viewsperday.'+req, expr), param, *(params+[datetime.combine(date.today(), time())]))
modify('COMMIT')
except Exception:
except Exception: #pylint: disable=broad-except
traceback.print_exc()
expr = queries[req].replace('%T', '"'+str(date.today())+'"')
expr = queries[req].replace('%T', '\''+str(date.today())+'\'')
rows = query(query_expr%('viewsperday.'+req, expr), param, *params)
start = None
traces = set()
data = {}
for row in rows:
if not start or row['date'] < start:
start = row['date']
row_date = row['date']
if isinstance(row_date, datetime):
row_date = row_date.date()
if not start or row_date < start:
start = row_date
traces.add(row['trace'])
if row['date'] not in data:
data[row['date']] = {}
data[row['date']][row['trace']] = row['y']
if row_date not in data:
data[row_date] = {}
data[row_date][row['trace']] = row['y']
end = date.today()
res = [{'name': trace, 'x': [], 'y': []} for trace in traces]
......@@ -146,4 +159,3 @@ def stats_viewsperday(req, param=""):
trace['y'].append(data.get(start, {}).get(trace['name'], 0))
start += timedelta(days=1)
return Response(json.dumps(res, default=plotly_date_handler), mimetype='application/json')
from server import *
import subprocess
from time import mktime
from email.utils import formatdate
from socket import gethostname
from ipaddress import ip_address, ip_network
import base64
from server import *
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
app.jinja_env.trim_blocks = True #pylint: disable=no-member
app.jinja_env.lstrip_blocks = True #pylint: disable=no-member
app.add_template_global(random.randint, name='randint')
app.add_template_global(datetime, name='datetime')
app.add_template_global(timedelta, name='timedelta')
......@@ -13,19 +17,24 @@ app.add_template_global(min, name='min')
app.add_template_global(max, name='max')
# get git commit
output = subprocess.check_output(['git', "log", "-g", "-1", "--pretty=%H # %h # %d # %s"]).decode('UTF-8').split('#', 3)
app.jinja_env.globals['gitversion'] = {'hash': output[1], 'longhash': output[0], 'branch': output[2], 'msg': output[3]}
GITOUTPUT = subprocess.check_output(['git', "log", "-g", "-1", "--pretty=%H#%h#%d#%s"]).decode('UTF-8').split('#', 3)
app.jinja_env.globals['gitversion'] = {'hash': GITOUTPUT[1], 'longhash': GITOUTPUT[0], 'branch': GITOUTPUT[2], 'msg': GITOUTPUT[3]} #pylint: disable=no-member
@app.url_defaults
def static_version_inject(endpoint, values):
if endpoint == 'static':
values['v'] = app.jinja_env.globals['gitversion']['longhash'] #pylint: disable=no-member
@app.template_global()
def ismod(*args):
return ('user' in session)
def ismod(*args): #pylint: disable=unused-argument
return 'user' in session
app.jinja_env.globals['navbar'] = []
app.jinja_env.globals['navbar'] = [] #pylint: disable=no-member
# 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, userendpoint=False, group=None, endpoint=None):
def register_navbar(name, iconlib='bootstrap', icon=None, userendpoint=False, group=None, endpoint=None): #pylint: disable=too-many-arguments
def wrapper(func):
urlendpoint = endpoint
if not endpoint:
......@@ -38,11 +47,11 @@ def register_navbar(name, iconlib='bootstrap', icon=None, userendpoint=False, gr
item['endpoint'] = urlendpoint
item['visible'] = not urlendpoint in mod_endpoints
item['name'] = name
app.jinja_env.globals['navbar'].append(item)
app.jinja_env.globals['navbar'].append(item) #pylint: disable=no-member
return func
return wrapper
csrf_endpoints = []
csrf_endpoints = [] #pylint: disable=invalid-name
def csrf_protect(func):
csrf_endpoints.append(func.__name__)
@wraps(func)
......@@ -53,7 +62,7 @@ def csrf_protect(func):
token = request.get_json()['_csrf_token']
else:
token = None
if not ('_csrf_token' in session) or (session['_csrf_token'] != token ) or not token:
if ('_csrf_token' not in session) or (session['_csrf_token'] != token) or not token:
return 'csrf test failed', 403
else:
return func(*args, **kwargs)
......@@ -69,11 +78,11 @@ def csrf_inject(endpoint, values):
def base64encode(str):
try:
return base64.b64encode(str.encode('UTF-8')).decode('UTF-8')
except:
except: #pylint: disable=bare-except
return ''
@app.template_filter()
def checkperm(perms, username=None, password=None):
def checkperm(perms, username=None, password=None): #pylint: disable=too-many-branches,too-many-return-statements
if ismod():
return True
perms = evalperm(perms)
......@@ -86,6 +95,9 @@ def checkperm(perms, username=None, password=None):
elif perm['type'] == 'l2p':
if perm['param1'] in session.get('l2p_courses', []):
return True
elif perm['type'] == 'moodle':
if perm['param1'] in session.get('moodle_courses', []):
return True
elif perm['type'] == 'rwth':
if session.get('rwthintern', False):
return True
......@@ -97,12 +109,31 @@ def checkperm(perms, username=None, password=None):
return True
return False
def checkperm_array(perms, auth_data):
for username, password in auth_data.items():
if checkperm(perms, username, password):
return True
# Authentication can also be performed without username/password
if checkperm(perms):
return True
return False
def permtypes(perms): #pylint: disable=too-many-branches,too-many-return-statements
perms = evalperm(perms)
perm_set = set()
for perm in perms:
perm_set.add(perm['type'])
return list(perm_set)
@app.template_filter()
def permdescr(perms):
def permdescr(perms): #pylint: disable=too-many-branches,too-many-return-statements
perms = evalperm(perms)
public = False
password = False
l2p_courses = []
moodle_courses = []
rwth_intern = False
fsmpi_intern = False
for perm in perms:
......@@ -110,8 +141,8 @@ def permdescr(perms):
public = True
elif perm['type'] == 'password':
password = True
elif perm['type'] == 'l2p':
l2p_courses.append(perm['param1'])
elif perm['type'] == 'moodle':
moodle_courses.append(perm['param1'])
elif perm['type'] == 'rwth':
rwth_intern = True
elif perm['type'] == 'fsmpi':
......@@ -124,35 +155,36 @@ def permdescr(perms):
return 'rwth', 'Nur für RWTH-Angehörige verfügbar'
if fsmpi_intern:
return 'fsmpi', 'Nur für Fachschaftler verfügbar'
if l2p_courses:
if moodle_courses:
if password:
return 'l2p', 'Nur für Teilnehmer der Veranstaltung und Nutzer mit Passwort verfügbar'
return 'l2p', 'Nur für Teilnehmer der Veranstaltung verfügbar'
return 'moodle', 'Nur für Teilnehmer der Veranstaltung und Nutzer mit Passwort verfügbar'
else:
return 'moodle', 'Nur für Teilnehmer der Veranstaltung verfügbar'
if password:
return 'password', 'Nur für Nutzer mit Passwort verfügbar'
return 'none', 'Nicht verfügbar'
# debian ships jinja2 without this test...
@app.template_test(name='equalto')
def equalto(a,b):
return a == b
def equalto(value_a, value_b):
return value_a == value_b
@app.template_filter(name='filterdict')
def jinja2_filterdict(value, attrdel):
v = dict(value)
for a in attrdel:
if a in v:
del v[a]
return dict(v)
value = dict(value)
for attr in attrdel:
if attr in value:
del value[attr]
return dict(value)
@app.template_filter(name='semester')
def human_semester(s, long=False):
if not s or s == 'zeitlos' or len(s) != 6:
def human_semester(value, long=False):
if not value or value == 'zeitlos' or len(value) != 6:
return 'Zeitlos'
year = s[0:4]
semester = s[4:6].upper()
year = value[0:4]
semester = value[4:6].upper()
if not year.isdigit() or semester not in ['SS', 'WS']:
print('Invalid semester string "%s"'%s)
print('Invalid semester string "%s"'%value)
return '??'
if not long:
return semester+year[2:]
......@@ -162,28 +194,28 @@ def human_semester(s, long=False):
return 'Wintersemester %s/%s'%(year, str(int(year)+1)[2:])
@app.template_filter(name='date')
def human_date(d):
return d.strftime('%d.%m.%Y')
def human_date(value):
return value.strftime('%d.%m.%Y')
@app.template_filter(name='fulldate')
def human_fulldate(d):
return d.strftime('%a, %d.%m.%Y, %H:%M Uhr')
def human_fulldate(value):
return value.strftime('%a, %d.%m.%Y, %H:%M Uhr')
@app.template_filter(name='time')
def human_time(d):
return d.strftime('%H:%M')
def human_time(value):
return value.strftime('%H:%M')
@app.template_filter()
def rfc3339(d):
return d.strftime('%Y-%m-%dT%H:%M:%S+02:00')
def rfc3339(value):
return value.strftime('%Y-%m-%dT%H:%M:%S+02:00')
@app.template_filter()
def time_offset(s):
return '%02d:%02d:%02d'%(s//3600, (s//60)%60, s%60)
def time_offset(value):
return '%02d:%02d:%02d'%(value//3600, (value//60)%60, value%60)
@app.template_filter()
def rfc822(d):
return formatdate(mktime(d.timetuple()))
def rfc822(value):
return formatdate(mktime(value.timetuple()))
@app.template_global()
def get_announcements(minlevel=0):
......@@ -191,29 +223,35 @@ def get_announcements(minlevel=0):
if ismod():
offset = timedelta(hours=24)
try:
return query('SELECT * FROM announcements WHERE NOT deleted AND ((time_expire = NULL) OR time_expire > ?) AND (? OR (visible AND time_publish < ?)) AND level >= ? ORDER BY level DESC', datetime.now()-offset, ismod(), datetime.now(), minlevel)
except:
return query('SELECT * FROM announcements WHERE \
NOT deleted AND \
((time_expire = NULL) OR time_expire > ?) AND \
(? OR (visible AND time_publish < ?)) AND \
level >= ? \
ORDER BY level DESC',
datetime.now()-offset, ismod(), datetime.now(), minlevel)
except: #pylint: disable=bare-except
return []
@app.template_filter()
def fixnl(s):
def fixnl(value):
# To be remove, as soon as db schema is cleaned-up
return str(s).replace('\n', '<br>')
return str(value).replace('\n', '<br>')
@app.template_filter()
def tagid(s):
if not s:
def tagid(value):
if not value:
return 'EMPTY'
s = s.replace(' ', '_').lower()
r = ''
for c in s:
if c in string.ascii_lowercase+string.digits+'_':
r = r + c
return r
value = value.replace(' ', '_').lower()
result = ''
for char in value:
if char in string.ascii_lowercase+string.digits+'_':
result = result + char
return result
@app.template_global()
def is_readonly():
try:
return show('SHOW GLOBAL STATUS LIKE "wsrep_ready"')['wsrep_ready'] != 'ON'
except:
return show('SHOW GLOBAL STATUS LIKE \'wsrep_ready\'')['wsrep_ready'] != 'ON'
except: #pylint: disable=bare-except
return True
......@@ -34,14 +34,14 @@
{% endif %}
<script src="{{url_for('static', filename='videojs/video.js')}}"></script>
<script src="{{url_for('static', filename='videojs/lang/de.js')}}"></script>
<script src="{{url_for('static', filename='videojs/ie8/videojs-ie8.js')}}"></script>
<script src="{{url_for('static', filename='videojs/videojs-resolution-switcher.js')}}"></script>
<script src="{{url_for('static', filename='videojs/videojs-contrib-hls.js')}}"></script>
<script src="{{url_for('static', filename='videojs/videojs.hotkeys.js')}}"></script>
<script src="{{url_for('static', filename='videojs/videojs-markers.js')}}"></script>
<script src="{{url_for('static', filename='hammer.min.js')}}"></script>
{% endblock %}
</head>
<body>
{% block body %}
{% block navbar %}
{% macro navbaricon(data, user=none) -%}
<li{% if data.endpoint == request.endpoint %} class="active"{% endif %}>
......@@ -81,6 +81,22 @@
{{ navbaricon(n) }}
{% endfor %}
<li>
<a href="https://beta.video.fsmpi.rwth-aachen.de" style="padding: 10px 6px; margin-left: 1em; color: rgb(10, 156, 10);">
<span aria-hidden="true" class="fa fa-external-link-square-alt"></span>
Zur neuen Seite
</a>
<script>
$(function() {
$('a[href="https://beta.video.fsmpi.rwth-aachen.de"]').on('click', function(e) {
e.preventDefault();
window.location.href = 'https://beta.video.fsmpi.rwth-aachen.de' + window.location.pathname;
});
});
</script>
</li>
{% for grouper, list in navbar|rejectattr("group", "none")|groupby("group") if ismod() %}
<li{% if request.endpoint in list|map(attribute='endpoint') %} class="active dropdown"{% endif %}>
<a data-toggle="dropdown" data-boundary="viewport" class="dropdown-toggle" style="padding: 10px 6px; cursor: pointer;">{{ grouper }}<span class="caret"></span></a>
......@@ -166,6 +182,9 @@
<li>
<a href="http://www.vampir.rwth-aachen.de/">Vampir e.V.</a>
</li>
<li>
<a href="/imprint">Impressum</a>
</li>
<li>
<a href="https://www.youtube.com/channel/UCxh5snRN7yZyBsytNbGNuEQ">Youtube</a>
</li>
......@@ -212,6 +231,7 @@
});
</script>
{% endif %}
{% endblock %}
<script>
$( function () {
$('[data-toggle="tooltip"]').tooltip(
......@@ -220,6 +240,18 @@
html: true
});
});
$(document).on("click","a.reloadonclose",function () {
var popup = window.open(this.href, this.target);
if (!popup)
return true;
var popup_check = setInterval(function() {
if (popup.closed) {
clearInterval(popup_check);
location.reload();
};
}, 500);
return false;
});
</script>
</body>
</html>
......@@ -57,6 +57,7 @@
</select>
</td></tr>
<tr><td>Interne Bemerkungen:</td><td>{{ moderator_editor(['courses',course.id,'internal'], course.internal) }}</td></tr>
<tr><td>Login-Informationen:</td><td>{{ moderator_editor(['courses',course.id,'login_info'], course.login_info) }}</td></tr>
</tbody>
</table>
</div>
......@@ -72,11 +73,11 @@
<div class="row panel-body collapse out panel-collapse" id="statspanel">
<div class="col-md-6 col-xs-12">
<p class="text-center">Zuschauer pro Tag</p>
<div class="plot-view" data-url="{{url_for('stats_viewsperday', req="course", param=course.id)}}"></div>
<div id="plot_stats_viewsperday" class="plot-view" data-url="{{url_for('stats_viewsperday', req="course", param=course.id)}}"></div>
</div>
<div class="col-md-6 col-xs-12">
<p class="text-center">Zuschauer pro Termin</p>
<div class="plot-view" data-type="bar" data-url="{{url_for('stats_generic', req="lecture_views", param=course.id)}}"></div>
<div id="plot_stats_generic" class="plot-view" data-type="bar" data-url="{{url_for('stats_generic', req="lecture_views", param=course.id)}}"></div>
</div>
</div>
</div>
......@@ -125,13 +126,14 @@
<option selected value="password">Passwort</option>
<option value="rwth">RWTH intern</option>
<option value="fsmpi">FSMPI intern</option>
<option value="l2p">L2P Lernraum</option>
<option value="moodle">Moodle Lernraum</option>
<option value="none">Kein Zugriff</option>
</select>
<input class="col-xs-12 passwordinput authuser" type="text" placeholder="Benutzername">
<input class="col-xs-10 passwordinput authpassword" type="text" placeholder="Passwort">
<button class="col-xs-2 passwordinput authpgen" type="button" onclick="$('.authpassword',this.parentNode).val(moderator.permissioneditor.randompw());"><span class="fa fa-refresh" aria-hidden="true"></span></button>
<input class="col-xs-12 authl2p" type="text" placeholder="Lernraum" style="display: none;">
<input class="col-xs-12 authmoodle" type="text" placeholder="Lernraum" style="display: none;">
<button class="col-xs-4" onclick="moderator.permissioneditor.addbtnclick(this)">Add</button>
<button class="col-xs-4" onclick="moderator.permissioneditor.updatebtnclick(this)">Update</button>
<button class="col-xs-4" onclick="moderator.permissioneditor.delbtnclick(this)">Delete</button>
......
......@@ -31,8 +31,9 @@
</div>
</div>
{% if courses %}
<div class="visible-xs" id="xs-check"></div>
{% if groupedby == 'semester' %}
{% if groupedby == 'semester' and courses|groupby(groupedby)|length > 1%}
{% set grouped_courses = courses|groupby(groupedby)|reverse|list %}
<script>
$(function () {
......@@ -47,6 +48,7 @@
<script>
$(function () {
if($("#xs-check").is(":visible")) {
/* Only expand first group */
$(".collapse[id!='{{grouped_courses[0].grouper|tagid}}']").removeClass('in');
}
});
......@@ -78,4 +80,5 @@
</div></div>
</div>
{% endfor %}
{% endif %}
{% endblock %}
......@@ -12,39 +12,36 @@
</span>
</div>
<div class="panel-body table-responsive">
<table class="table table-condensed table-bordered">
<table id="cutprogress" class="table table-condensed table-bordered">
<tr>
<th class="text-left">
Datum
</th>
{% for course in courses %}
<th class="text-center rotate">
<th class="rotate">
<div>
<a title="Zuständig: {{ course.responsible|join(attribute='realname') }}" href="{{ url_for("course", handle=course.handle) }}">{{ course.short }}</a>
<span><a title="Zuständig: {{ course.responsible|join(attribute='realname') }}" href="{{ url_for("course", handle=course.handle) }}">{{ course.short }}</a></span>
</div>
</th>
{% endfor %}
</tr>
{% for i in range(maxlecturecount) %}
<tr class="text-center">
{% for course in courses %}
{% for row in tablebody %}
<tr class="text-center {% if row.is_new_week %}weekbreak{% endif %}">
<td class="text-left">{{ row.date.strftime("%d.%m.%Y (%a)") }}</td>
{% for cell in row.cells %}
<td>
{% set l = course.lectures[i]|d({}) %}
{% if "time" in l %}
<a href="{{ url_for("course", handle=course.handle) }}#lecture-{{ l.id }}" title="{{ l.time }} &#10;Titel: {{ l.title|replace('\n','') }} &#10;Videos: {{ l.videos|count }} &#10;Internes Kommentar: {{ l.internal }}">
{% if l.time < datetime.now() %}
{% if l.videos|count == 0 %}
{% for lecture in cell %}
<a href="{{ url_for('course', handle=lecture.course_id) }}#lecture-{{ lecture.id }}" title="Uhrzeit: {{ lecture.time.strftime('%H:%M') }}&#10;Titel: {{ lecture.title|replace('\n','') }}&#10;Videos: {{ lecture.videos_total }}&#10;Interner Kommentar: {{ lecture.internal }}">
{% if lecture.videos_total == 0 %}
<span style="color: red" aria-hidden="true" class="fa fa-times"></span>
{% else %}
{% if l.videos|selectattr('visible')|list|count == 0 %}
{% elif lecture.videos_visible == 0 %}
<span style="color: orange" aria-hidden="true" class="glyphicon glyphicon-ok"></span>
{% else %}
<span style="color: green" aria-hidden="true" class="glyphicon glyphicon-ok"></span>
{% endif %}
{% endif %}
{% else %}
<span style="color: grey" aria-hidden="true" class="fa fa-times"></span>
{% endif %}
</a>
{% else %}
{% endif %}
{% endfor %}
</td>
{% endfor %}
</tr>
......
......@@ -4,17 +4,12 @@
{% extends "base.html" %}
{% block title %}- {{ course.title }}: {{ lecture.title}}{% endblock %}
{% block navbar %}
{% endblock %}
{% block alerts %}
{% endblock %}
{% block content %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-xs-12" style="padding: 0px">
{{ player(lecture, videos, get_flashed_messages(category_filter=['player','message']), seek=seek) }}
</div>
</div>
{% endblock %}
{% block footer %}
</div>
{% endblock %}
......@@ -53,10 +53,10 @@
<ul>
<li><strong>Öffentliche</strong> Videos (<span class="fa fa-globe-asia"></span>) können von jedem abgespielt werden.</li>
<li><strong>RWTH-interne</strong> Videos (<span class="fa" style="width: 25px; height: 17px; background-size: cover; background-image: url('/static/rwth.png');"></span>) sind nur für RWTH-Angehörige verfügbar. Zum Anschauen ist eine Authentifizierung über das RWTH-Single-Sign-On nötig.</li>
<li><strong>Lernraum-interne</strong> Videos (<span class="fa" style="width: 12px; height: 14px; background-size: cover; background-image: url('/static/l2p-logo.gif');"></span>) sind nur für Teilnehmer eines bestimmten L2P-Lernraums zugänglich. Wir überprüfen dies, indem du uns kurzzeitig Zugriff auf deinen L2P-Account gibst, aus dem wir die Liste deiner Lernräume auslesen. Solltest du eine Vorlesung hören, dem zugehörigen Lernraum aber nicht hinzugefügt worden sein, melde dich bitte beim entsprechenden Dozenten.</li>
<li><strong>Lernraum-interne</strong> Videos (<span class="fa" style="width: 12px; height: 14px; background-size: cover; background-image: url('/static/l2p-logo.gif');"></span> oder <span class="fa" style="width: 20px; height: 14px; background-size: cover; background-image: url('/static/moodle.png');"></span>) sind nur für Teilnehmer eines bestimmten L2P- oder Moodle-Lernraums zugänglich. Wir überprüfen dies, indem du uns kurzzeitig Zugriff auf deinen L2P- oder Moodle-Account gibst, aus dem wir die Liste deiner Lernräume auslesen. Solltest du eine Vorlesung hören, dem zugehörigen Lernraum aber nicht hinzugefügt worden sein, melde dich bitte beim entsprechenden Dozenten.</li>
<li><strong>Passwort-geschützte</strong> Videos (<span class="fa fa-lock"></span>) sind nur nach Eingabe eines Passworts verfügbar, welches jedem zur Verfügung gestellt wurde, der Zugriff haben sollte. In der Regel gibt es gute Gründe, warum dieser Zugriffsschutz gewählt wurde.</li>
</ul>
<p>Falls du keinen Zugriff auf ein Video hast, wird dies auf der Player-Seite angezeigt und ggf. auf die weiteren Schritte zur Authentifizierung verwiesen. Die erläuterten Möglichkeiten decken nicht jeden denkbaren Fall ab. Solltest du Zugriff auf eine Veranstaltung benötigen, aber aus irgendwelchen Gründen nicht haben, schreib uns eine Mail.<p>
<p>Falls du keinen Zugriff auf ein Video hast, wird dies auf der Player-Seite angezeigt und ggf. auf die weiteren Schritte zur Authentifizierung verwiesen. Die erläuterten Möglichkeiten decken nicht jeden denkbaren Fall ab. Solltest du Zugriff auf eine Veranstaltung benötigen, aber aus irgendwelchen Gründen nicht haben, schreib uns eine Mail.</p>
{% endcall %}
{% call faqentry("playback", "Ich kann ein heruntergeladenes Video nicht abspielen.") %}
<p>Unsere neueren Videos sind nach MPEG 4 AVC (H.264) kodiert. Diese Kodierung produziert gute Bildqualität und spart Platz. Außerdem sollten unsere Videos so in den meinsten Playern funktionieren.</p>
......@@ -66,15 +66,15 @@
{% endcall %}
<div class="faqHeader">Technisches</div>
{% call faqentry("l2prights", "Warum benötigt ihr lesenden und schreibenden Zugriff auf meine L2P-Lernräume?") %}
<p>Auf vielfachen Wunsch unserer Dozenten haben wir die Möglichkeit umgesetzt, Videos nur für Teilnehmer eines bestimmten L2P-Lernraums zugänglich zu machen.
Um dies umzusetzen benötigen wir die Liste der Lernräume eines Nutzers, und damit Zugriff auf den L2P-Account.
Leider erlaubt uns das L2P nur entweder keinen oder vollen (also lesenden und schreibenden) Zugriff, sodass du uns zur Authentifizierung für Lernraum-interne Videos mehr Zugriffsrechte geben musst, als theoretisch nötig.</p>
{% call faqentry("l2prights", "Warum benötigt ihr lesenden und schreibenden Zugriff auf meine L2P oder Moodle-Lernräume?") %}
<p>Auf vielfachen Wunsch unserer Dozenten haben wir die Möglichkeit umgesetzt, Videos nur für Teilnehmer eines bestimmten L2P- oder Moodle-Lernraums zugänglich zu machen.
Um dies umzusetzen benötigen wir die Liste der Lernräume eines Nutzers, und damit Zugriff auf den L2P- oder Moodle-Account.
Leider erlaubt uns das L2P sowie Moodle nur entweder keinen oder vollen (also lesenden und schreibenden) Zugriff, sodass du uns zur Authentifizierung für Lernraum-interne Videos mehr Zugriffsrechte geben musst, als theoretisch nötig.</p>
<p>Um die Missbrauchsgefahr zu minimieren, widerrufen wir die Zugriffsrechte auf deine Lernräume, sobald wir die Liste der Lernräume abgerufen haben.
Du kannst dies <a href="https://oauth.campus.rwth-aachen.de/manage/">hier</a> überprüfen.
Außerdem speichern wir keine Authentifikationsdaten auf unserem Server.
</p>
<p>Details zu der dafür verwendeten Schnittstelle findest du in der <a href="https://www3.elearning.rwth-aachen.de/_vti_bin/L2PServices/api.svc/v1/Documentation">L2P-API-Dokumentation</a>.</p>
<p>Details zu der dafür verwendeten Schnittstelle findest du in der <a href="https://www3.elearning.rwth-aachen.de/_vti_bin/L2PServices/api.svc/v1/Documentation">L2P-API-Dokumentation</a> und der <a href="https://moped.ecampus.rwth-aachen.de/proxy/api/v2/documentation">Moodle-API-Dokumentation</a>.</p>
{% endcall %}
{% call faqentry("cookies", "Welche Cookies werden gesetzt und wofür?") %}
<p>Beim reinen Betrachten der Seite setzen wir überhaupt keine Cookies. Einzig beim Abruf oder Abspielen von Videodateien und bei der Authentifizierung für RWTH- oder Lernraum-interne Videos werden Cookies gesetzt.</p>
......@@ -82,6 +82,23 @@
<p>Anders sieht dies beim <code>session</code>-Cookie aus. Für die Authentifizierung ist es notwendig dieses zuzulassen, da wir alle nötigen Daten (OAuth-Tokens, Zugriffsrechte) in diesem Cookie und nicht auf unserem Server speichern. Das Cookie ist kryptographisch gesichert und enthält nur die zum jeweiligen Zeitpunkt nötigen Daten.</p>
<p>In keinem Fall erstellen wir auf Basis der gespeicherten Daten Nutzerprofile.</p>
{% endcall %}
<div class="faqHeader">Video Player</div>
{% call faqentry("playershortcuts", "Welche Shortcuts gibt es für den Player?") %}
<p>Es gibt folgende Shortcuts um den Player mit der Tastatur zu Steuern:</p>
<ul>
<li><strong>Space</strong> um das Video zu Pausieren.</li>
<li><strong>Pfeiltasten Links/Rechts</strong> um im Video 15 Sekunden zurück oder vorzuspulen.</li>
<li><strong>Pfeiltasten Hoch/Runter</strong> um die Lautstärke zu erhöhen oder zu veringern.</li>
<li><strong>M</strong> um das Video auf Stumm zu stellen.</li>
<li><strong>F</strong> um das Video im Fullscreen anzuzeigen.</li>
<li><strong>0-9</strong> um zur x Prozent Marke des Videos zu Springen. So springt man zum Beispiel mit 5 zur Hälfte des Videos.</li>
</ul>
{% endcall %}
{% call faqentry("embedding", "Wie kann ich Videos auf anderen Webseiten einbetten?") %}
<p>Unter jedem Video befindet sich auf der rechten Seite ein „Einbetten“-Button. Wenn du diesen Button anklickst, wird ein Einbettcode angezeigt, mit dem du das Video in eine andere Webseite einbetten kannst.</p>
<p><strong>Einbettung in Moodle-Lernräumen:</strong> Der Einbettcode kann auch auf Textseiten in RWTHmoodle eingefügt werden. Dazu muss zuerst die Menüleiste des Editors über den Button „Menüleiste umschalten“ bzw. „Mehr Symbole anzeigen“ (erkennbar am einem Pfeil-Symbol) umgeschaltet werden. Danach wird der Button „HTML“ (<code>&lt;/&gt;</code>) sichtbar, über den der Editor in den HTML-Modus umgeschaltet werden muss. Danach kann der Einbettcode an der gewünschten Stelle eingefügt werden.</p>
{% endcall %}
</div>
<script>
......
{% extends "base.html" %}
{% block content %}
<div class="alert alert-warning alert-dismissible" role="alert" id="kontakt">
Unter <a href="mailto:video@fsmpi.rwth-aachen.de">video@fsmpi.rwth-aachen.de</a> stehen wir für alle Fragen bereit.
</div>
<div class="panel-group" id="accordion">
<h2>Impressum</h2>
<p>Die Video-AG ist eine Arbeitsgemeinschaft der <a href="https://www.fsmpi.rwth-aachen.de/">Fachschaft Mathe/Physik/Informatik (I/1)</a>, Teilkörperschaft der RWTH Aachen.</p>
<div class="row">
<div class="col-md-6">
<h3>Hausadresse</h3>
<div style="margin-left: 2em;">
Fachschaft Mathematik/Physik/Informatik an der RWTH Aachen<br>
Augustinerbach 2a<br>
52062 Aachen<br>
Germany
</div>
</div>
<div class="col-md-6">
<h3>Postadresse</h3>
<div style="margin-left: 2em;">
Studierendenschaft der RWTH Aachen<br>
Fachschaft Mathematik/Physik/Informatik<br>
Templergraben 55<br>
52056 Aachen<br>
Germany
</div>
</div>
</div>
Die Video-AG ist ebenfalls unter <a href="mailto:video@fsmpi.rwth-aachen.de">video@fsmpi.rwth-aachen.de</a> erreichbar.<br><br>
Inhaltlich Verantwortlicher: Fachschaft Mathematik/Physik/Informatik der RWTH Aachen, Teilkörperschaft des öffentlichen Rechts.<br>
Sollte bei Teilen des Inhalts eine andere Person verantwortlich sein, so ist dies entsprechend gekennzeichnet.
<h4>Haftungshinweis</h4>
Trotz sorgfältiger inhaltlicher Kontrolle übernehmen wir keine Haftung für die Inhalte externer Links. Für den Inhalt der verlinkten Seiten sind ausschließlich deren BetreiberInnen verantwortlich.
</div>
{% endblock %}
......@@ -113,7 +113,7 @@
<ul class="list-group" style="margin: 0px;">
{% for i in g.list %}
<li class="list-group-item list-group-item-condensed">
{{i.time|time}} <a href="{{url_for('course', handle=i.course.handle)}}">{{i.course.title}}</a>: <a href="{{url_for('course', handle=i.course.handle)}}#lecture-{{i.id}}">{{i.title}}</a> {{livelabel(i.live, i.nowlive)}}
{{i.time|time}} <a href="{{url_for('course', handle=i.course.handle)}}">{{i.course.title}}</a>: <a href="{{url_for('course', handle=i.course.handle)}}#lecture-{{i.id}}">{{i.title}}</a> ({{i.place}}) {{livelabel(i.live, i.nowlive)}}
</li>
{% endfor %}
</ul>
......
{% from 'macros.html' import player %}
{% from 'macros.html' import authorize_helper %}
{% from 'macros.html' import video_download_btn %}
{% from 'macros.html' import video_embed_btn %}
{% from 'macros.html' import vtttime %}
......@@ -20,41 +21,42 @@
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="panel-title"><strong><a href="{{url_for('course', handle=course.handle)}}#lecture-{{lecture.id}}">{{ course.title }}</a></strong>: {{ lecture.title}}</span>
<span class="panel-title"><strong><a href="{{url_for('course', handle=course.handle)}}#lecture-{{lecture.id}}">{{ course.title }}</a></strong>: {{ lecture.title }} ({{ lecture.time.date().strftime("%a, %d.%m.%Y")}})</span>
</div>
<div class="panel-body">
<div class="row" style="padding: 0px;">
<div class="col-xs-12" style="padding-bottom: 5px;">
<div class="col-xs-12" style="padding-bottom: 15px;">
<a href="{{url_for('course', handle=course.handle)}}#lecture-{{lecture.id}}" class="btn btn-default" ><span class="fa fa-chevron-circle-left" aria-hidden="true"></span> Zur Veranstaltungsseite</a>
<ul class="list-inline pull-right">
<li>{{ video_embed_btn(lecture.id, course=course.handle) }}</li>
<li class="dropdown">{{ video_download_btn(videos) }}</li>
</ul>
</div>
</div>
<div class="row">
{% if isAuthorized %}
<div class="col-xs-12" style="padding: 0px">
{{ player(lecture, videos, get_flashed_messages(category_filter=['player']), seek=seek) }}
</div>
<div class="col-xs-12" style="padding-top: 20px">
<p>{{ moderator_editor(['lectures',lecture.id,'comment'], lecture.comment) }}</p>
{% else %}
{{ authorize_helper(course.login_info, permtypes, passwordSet, lecture, course, courses_loaded) }}
{% endif %}
<div class="col-xs-12" style="padding-top: 15px;">
<button class="btn btn-default" id="hintnewchapter">{% if ismod() %}Neues Kapitel{% else %}Kapitelmarker vorschlagen{% endif %}</button>
<ul class="list-inline pull-right" style="margin-bottom: 0px;">
<li>{{ video_embed_btn(lecture.id, course=course.handle) }}</li>
<li class="dropup">{{ video_download_btn(videos) }}</li>
</ul>
</div>
{% if lecture.comment or ismod() %}
<div class="col-xs-12" style="padding-top: 15px">
<h4><strong>Beschreibung:</strong></h4><p style="padding-left: 5px;padding-right: 5px"> {{ moderator_editor(['lectures',lecture.id,'comment'], lecture.comment) }}</p>
</div>
{% endif %}
{% if chapters %}
<div class="col-xs-12 table-responsive" style="padding-top: 10px;">
<p>Kapitel:
<button class="btn btn-default" id="hintnewchapter">{% if ismod() %}Neues Kapitel{% else %}Kapitelmarker vorschlagen{% endif %}</button>
</p>
<h4><strong>Kapitel:</strong></h4>
<table class="table table-hover">
<tr>
<th style="width: 130px;">Start</th>
<th>Kapitel</th>
{% if ismod() %}
<th>Sichtbar</th>
<th></th>
{% endif %}
</tr>
{% for c in chapters|sort(attribute='time') %}
<tr>
<td>
<td style="width: 130px;">
<a class="chapterlink" href="{{url_for('lecture', course=course.handle, id=lecture.id, t=c['time'])}}" data-seek-time="{{c['time']}}">{{ c.time|time_offset }}</a>
<br>
{% if ismod() %}
......@@ -70,6 +72,7 @@
{% endfor %}
</table>
</div>
{% endif %}
</div>
</div>
</div>
......@@ -82,11 +85,11 @@
<div class="row panel-body collapse out panel-collapse" id="statspanel">
<div class="col-md-6 col-xs-12">
<p class="text-center">Zuschauer pro Tag</p>
<div class="plot-view" data-url="{{url_for('stats_viewsperday', req="lecture", param=lecture.id)}}"></div>
<div id="plot_stats_viewsperday" class="plot-view" data-url="{{url_for('stats_viewsperday', req="lecture", param=lecture.id)}}"></div>
</div>
<div class="col-md-6 col-xs-12">
<p class="text-center">Zuschauer im Livestream</p>
<div class="plot-view" data-url="{{url_for('stats_generic', req="live_views", param=lecture.id)}}" data-reload="60000"></div>
<div id="plot_stats_generic" class="plot-view" data-url="{{url_for('stats_generic', req="live_views", param=lecture.id)}}" data-reload="60000"></div>
</div>
</div>
</div>
......@@ -116,7 +119,7 @@ $(function() {
{
html:true,
title:'Kapitelmarkierung vorschlagen',
placement: 'bottom',
placement: 'top',
container: 'body',
content: function() {
var zeropad = function (num, places) {
......@@ -128,24 +131,13 @@ $(function() {
var m = zeropad( Math.trunc((timestamp%3600)/60),2);
var s = zeropad( Math.trunc(timestamp%60),2);
var timeasstring = h+':'+m+':'+s;
return '<form class="needs-validation" method="post" data-url="{{ url_for('suggest_chapter', lectureid=lecture.id) }}" onSubmit="return hintchapterclick(this);"><input class="form-control" placeholder="00:00" name="time" type="text" value="'+timeasstring+'" pattern="(|[0-9]{1,2}:(|[0-9]{1,2}:))[0-9]{1,2}"><br><input class="form-control" placeholder="Titel" name="text" type="text" required><br><input type="submit" class="btn btn-default" value="{% if ismod() %}Hinzufügen{% else %}Vorschlagen{% endif %}"></form>';
return '<form class="needs-validation" method="post" data-url="{{ url_for('suggest_chapter', lectureid=lecture.id) }}" onSubmit="return hintchapterclick(this);"><input class="form-control" style="margin-top: 6px;margin-bottom: 6px;" placeholder="00:00" name="time" type="text" value="'+timeasstring+'" pattern="(|[0-9]{1,2}:(|[0-9]{1,2}:))[0-9]{1,2}"><input class="form-control" placeholder="Titel" name="text" type="text" style="margin-bottom: 15px;"required><input type="submit" class="btn btn-default" style="margin-bottom: 6px;" value="{% if ismod() %}Hinzufügen{% else %}Vorschlagen{% endif %}"></form>';
}
})
});
$(document).ready(function() {
$("a.reloadonclose").click(function () {
var popup = window.open(this.href, this.target);
if (!popup)
return true;
var popup_check = setInterval(function() {
if (popup.closed) {
clearInterval(popup_check);
location.reload();
};
}, 500);
return false;
});
$("a.chapterlink").click(function () {
videojs('videoplayer').currentTime($(this).data("seek-time"));
return false;
......
......@@ -54,7 +54,7 @@
{% else %}
{% set mfrag = "#t="+seek %}
{% endif %}
<video id="videoplayer" style="width: 100%" class="video-js vjs-default-skin vjs-big-play-centered" width="640" height="320" controls data-wasnotplayed="1" data-setup='{ "language":"de", "plugins" : {"hotkeys": {"seekStep": 15, "enableVolumeScroll": false, "alwaysCaptureHotkeys": true}, "videoJsResolutionSwitcher": { "ui": true, "default": "720p", "dynamicLabel": false } }, "customControlsOnMobile": true, "playbackRates": [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4] }'>
<video id="videoplayer" style="width: 100%" class="video-js vjs-big-play-centered" width="640" height="320" controls data-wasnotplayed="1" data-setup='{"language": "de", "userActions": {"hotkeys": false}, "playbackRates": [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4], "controlBar": {"pictureInPictureToggle": false}, "plugins": {"hotkeys": {"seekStep": 15, "enableVolumeScroll": false, "alwaysCaptureHotkeys": true}, "videoJsResolutionSwitcher": { "ui": true, "default": "720p", "dynamicLabel": false }}}'>
{% for v in videos|sort(attribute='formats.player_prio', reverse=True) %}
<source type="{{ v.formats.mimetype }}" src="{{ config.VIDEOPREFIX }}/{{ v.path }}{{mfrag}}" data-label="{{ v.formats.description }}" data-res="{{v.formats.resolution}}" data-aspect="{{v.formats.aspect}}" data-player_prio="{{v.formats.player_prio}}"/>
{% endfor %}
......@@ -64,6 +64,26 @@
$(function() {
$('#videoplayer').addClass("vjs-fluid");
$('#videoplayer').css("width");
var videoobj = document.getElementById("videoplayer");
var player = videojs("videoplayer");
var manager = new Hammer.Manager(videoobj);
var DoubleTap = new Hammer.Tap({
event: 'doubletap',
taps: 2
});
manager.add(DoubleTap);
manager.on('doubletap', function(e) {
if((e.target.clientHeight-e.center.y)>document.getElementsByClassName("vjs-control-bar")[0].clientHeight) {
if(e.center.x<(e.target.clientWidth/2)) {
player.currentTime(player.currentTime() - 15);
} else {
player.currentTime(player.currentTime() + 15);
}
}
});
videojs("videoplayer").ready(function() {
//resume
var progress_key = "progress_{{ lecture.id }}";
......@@ -141,6 +161,62 @@ $(function() {
</script>
{% endmacro %}
{% macro authorize_helper(login_info, permtypes, passwordSet, lecture, course, courses_loaded) %}
<div class="col-xs-12" style="padding: 10px; background-color: black; color:white;">
<h3 class="text-center mb2">Anmeldung erforderlich</h3>
{% if login_info %}
<p class="text-center">{{ login_info | safe }}</p>
{% endif %}
<div style="padding-bottom: 20px" class="container-fluid">
<div class="row">
{% if 'password' in permtypes %}
<div class="col-sm-4">
<h4 class="text-center"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Benutzername/Passwort</h4>
{% if passwordSet %}
<p class="alert alert-warning">Das aktuell verwendete Passwort ist nicht gültig.</p>
{% endif %}
<form method="POST" action="{{url_for('sessionLogin', course=course.handle, id=lecture.id)}}">
<div class="form-group">
<label for="exampleInputEmail1">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" placeholder="">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Passwort</label>
<input type="password" class="form-control" id="password" name="password" placeholder="">
</div>
<button type="submit" class="btn btn-default">Anmelden</button>
</form>
</div>
{% endif %}
{% if 'rwth' in permtypes %}
<div class="col-sm-4">
<h4 class="text-center"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> RWTH</h4>
<p>Für RWTH-Angehörige und aus dem RWTH-Netz verfügbar</p>
<a target="_blank" href="{{ url_for('start_rwthauth') }}" class="btn btn-default reloadonclose">Anmelden</a>
</div>
{% endif %}
{% if 'moodle' in permtypes %}
<div class="col-sm-4">
<h4 class="text-center"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Moodle</h4>
<p>Für Teilnehmer der Veranstaltung verfügbar</p>
{% if 'moodle' in permtypes %}
{% if not courses_loaded %}
<a target="_blank" href="{{ url_for('start_moodleauth') }}" class="btn btn-default reloadonclose">Anmelden</a>
{% else %}
<p class="alert alert-info">Du bist kein Teilnehmer des Moodle-Kurses!</p>
<a target="_blank" href="{{ url_for('start_moodleauth') }}" class="btn btn-default reloadonclose">Kurse aktualisieren</a>
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
{% if 'moodle' not in permtypes and 'rwth' not in permtypes and 'password' not in permtypes %}
<p class="alert alert-info" style="margin-top: 2em;">Nur für Fachschaftler verfügbar.</p>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro course_list_item(course,show_semester=False) %}
<li class="list-group-item list-group-item-condensed {% if (not course.visible) or (not course.listed) %}list-group-item-danger{% endif %}">
<div class="row">
......@@ -174,7 +250,7 @@ $(function() {
<button class="btn btn-default dropdown-toggle{% if not videos|selectattr('downloadable')|list and not ismod() %} disabled{% endif %}" type="button" data-toggle="dropdown">Download <span class="caret"></span></button>
<ul class="dropdown-menu">
{% for v in videos|sort(attribute='formats.prio', reverse=True) if (v.downloadable or ismod() ) %}
<li><a href="{{ config.VIDEOPREFIX }}/{{v.path}}">{{v.formats.description}} ({{v.file_size|filesizeformat(true)}})</a></li>
<li><a href="{{ config.VIDEOPREFIX }}/{{v.path}}" download>{{v.formats.description}} ({{v.file_size|filesizeformat(true)}})</a></li>
{% endfor %}
</ul>
{% endif %}
......@@ -183,7 +259,17 @@ $(function() {
{% endif %}
<ul class="pull-right list-unstyled" style="margin-left:10px;">
{% for v in videos|sort(attribute='formats.prio', reverse=True) if (v.downloadable or ismod() ) %}
<li>{{moderator_delete(['videos',v.id,'deleted'])}} {{ moderator_checkbox(['videos',v.id,'visible'], v.visible) }} <a href="{{ config.VIDEOPREFIX }}/{{v.path}}">{{v.formats.description}} ({{v.file_size|filesizeformat(true)}})</a></li>
<li>
{{moderator_delete(['videos',v.id,'deleted'])}}
{{ moderator_checkbox(['videos',v.id,'visible'], v.visible) }}
<a href="{{ config.VIDEOPREFIX }}/{{v.path}}" download>{{v.formats.description}} ({{v.file_size|filesizeformat(true)}})</a>
{% if v.source %}
<a href="{{url_for('add_reencode_job', ref=request.url, videoid=v.id)}}" class="btn btn-default" data-toggle="tooltip" title="Video neu transcoden">
<span class="glyphicon glyphicon-refresh"></span>
</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% if not ismod() %}
......@@ -196,11 +282,12 @@ $(function() {
<span>Einbetten</span>
</a>
<script>
{% set embedcode = '<iframe width="700" height="394" src="'+url_for('embed', course=course, id=lectureid, _external=True)+'" frameborder="0" allowfullscreen="true"></iframe>' %}
{% set embedcode = '<div style="position:relative;padding-top:56.25%;"><iframe style="position:absolute;top:0;left:0;width:100%;height:100%;overflow: hidden;" src="'+url_for('embed', course=course, id=lectureid, _external=True)+'" frameborder="0" allowfullscreen="true" scrolling="no"></iframe></div>' %}
$('#embedcodebtn').popover(
{
html:true,
title:'Einbettcode',
placement: 'top',
content:'<span><input type="text" onclick="this.select()" value="{{embedcode}}"></span>'
}
)
......@@ -346,6 +433,9 @@ $('#embedcodebtn').popover(
{% if permdescription[0] == 'l2p' %}
{% set permlogos = '<span class="fa" aria-hidden="true" style="width: 12px; height: 14px; background-size: cover; background-image: url(\'/static/l2p-logo.gif\');"></span>' %}
{% endif %}
{% if permdescription[0] == 'moodle' or permdescription[0] == 'l2pandmoodle' %}
{% set permlogos = '<span class="fa" aria-hidden="true" style="width: 20px; height: 14px; background-size: cover; background-image: url(\'/static/moodle.png\');"></span>' %}
{% endif %}
{% if permdescription[0] == 'rwth' %}
{% set permlogos = '<span class="fa" aria-hidden="true" style="width: 25px; height: 20px; background-size: cover; background-image: url(\'/static/rwth.png\');"></span>' %}
{% endif %}
......
......@@ -6,7 +6,7 @@
<div class="panel-heading">
<h1 class="panel-title">Sortierlog
<a class="btn btn-default" href="{{url_for('sort_now', ref=request.url)}}">Jetzt einsortieren</a>
<button class="btn btn-default" onclick="$('button[data-path^=\'sorterrorlog.\'][data-path$=\'.deleted\']').each(function (e) { moderator.api.set($(this).data('path'),1,false); }); window.location.reload();">Alle Fehler entfernen</button>
<button class="btn btn-default" onclick="$('button[data-path^=\'sorterrorlog.\'][data-path$=\'.deleted\']').each(function (e) { moderator.api.set($(this).data('path'),true,false); }); window.location.reload();">Alle Fehler entfernen</button>
</h1>
</div>
<div class="panel-body">
......
......@@ -9,19 +9,19 @@
<div class="row col-xs-12">
<div class="col-xs-12 col-md-6">
<p class="text-center">Veranstaltungen pro Semester</p>
<div class="plot-view" data-url="{{url_for('stats_generic', req="course_count")}}"></div>
<div id="plot_course_count" class="plot-view" data-url="{{url_for('stats_generic', req="course_count")}}"></div>
</div>
<div class="col-xs-12 col-md-6">
<p class="text-center">Vorlesungen pro Semester</p>
<div class="plot-view" data-url="{{url_for('stats_generic', req="lectures_count")}}"></div>
<div id="plot_lectures_count" class="plot-view" data-url="{{url_for('stats_generic', req="lectures_count")}}"></div>
</div>
<div class="col-xs-12 col-md-6">
<p class="text-center">Veranstaltungen pro Kategorie</p>
<div class="plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="categories_courses")}}"></div>
<div id="plot_categories_courses" class="plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="categories_courses")}}"></div>
</div>
<div class="col-xs-12 col-md-6">
<p class="text-center">Vorlesungen pro Kategorie</p>
<div class="plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="categories_lectures")}}"></div>
<div id="plot_categories_lectures" class="plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="categories_lectures")}}"></div>
</div>
<!--<div class="col-xs-12 col-md-12 plot-view" style="height: 1200px;" data-type="pie" data-url="{{url_for('stats_generic', req="organizer_courses")}}"></div>!-->
</div>
......@@ -34,11 +34,11 @@
<div class="panel-body" >
<div class=col-xs-12">
<p class="text-center">Zuschauer pro Veranstaltung</p>
<div class="plot-view" data-url="{{url_for('stats_viewsperday', req="courses", filter=filter)}}"></div>
<div id="plot_courses" class="plot-view" data-url="{{url_for('stats_viewsperday', req="courses", filter=filter)}}"></div>
</div>
<div class=col-xs-12">
<p class="text-center">Zuschauer pro Format</p>
<div class="plot-view" data-url="{{url_for('stats_viewsperday', req="global", filter=filter)}}"></div>
<div id="plot_global" class="plot-view" data-url="{{url_for('stats_viewsperday', req="global", filter=filter)}}"></div>
</div>
</div>
</div>
......
......@@ -42,7 +42,7 @@
<table id="timetable" class="table table-bordered col-xs-12" style="width: auto; min-width: 100%;">
<tr><th style="width: 30px;"></th>{% for d in days if (d.index < 5) or (d.lectures|length) > 0 %}<th style="min-width: 10em;" colspan="{{d.maxcol}}">{{ d.date.strftime("%A (%d.%m.%Y)") }}</th>{% endfor %}</tr>
{# iterating over each 15 min block #}
{% for t in times %}
{% for t in blocks %}
{% set time_index = loop.index %}
<tr{% if t.strftime("%M") == "00" %} class="hourlytime"{% endif %}>
{# display time in first row if its a full hour #}
......@@ -51,9 +51,9 @@
{% for d in days if (d.index < 5) or (d.lectures|length) > 0 %}
{% for col in range(1,d.maxcol+1) %}
{# iterate over all lextures but only consider those that are in the current column and happen in the 15 min block #}
{# iterate over all lectures but only consider those that are in the current column and happen in the 15 min block #}
{# time_index starts at 0 so we use it directly and do not do +1 #}
{% for l in d.lectures|selectattr('timetable_col','equalto',col) if ((l.time.time() >= t) and (l.time.time() < times[time_index])) %}
{% for l in d.lectures|selectattr('timetable_col','equalto',col) if ((l.time.time() >= t) and (l.time.time() < blocks[time_index])) %}
{# handle the first column of a day specialy, set red background if hidden #}
<td
{% if col == 1 %} class="newday"{% endif %}
......
......@@ -14,7 +14,7 @@ class JobmanagementTestCase(FlaskTestCase):
return data[0]['count']
def getCanceledJobCount(self):
data = query("SELECT count(id) AS count from jobs WHERE canceled=1")
data = query("SELECT count(id) AS count from jobs WHERE canceled=true")
return data[0]['count']
def generateTestJob(self):
......
......@@ -61,6 +61,12 @@ class VideoTestCase(unittest.TestCase):
r = c.get('/15ws-einfprog')
assert r.status_code == 200
r = c.get('/99')
assert r.status_code == 200
r = c.post('/99/2330/login', data={"username": "wrong", "password": "auth"})
assert r.status_code == 302
def test_timetable(self):
with self.app as c:
r = c.get('/internal/timetable')
......@@ -73,7 +79,6 @@ class VideoTestCase(unittest.TestCase):
assert r.status_code == 200
assert 'AfI' in r.data.decode()
assert 'Progra' in r.data.decode()
assert 'Bio' in r.data.decode()
def test_faq(self):
r = self.app.get('/faq')
......@@ -118,7 +123,7 @@ class VideoTestCase(unittest.TestCase):
def test_search(self):
r = self.app.get('/search?q=Malo')
assert r.status_code == 200
assert 'Mathematische Logik II' in r.data.decode() and '4.1 Der Sequenzenkalkül' in r.data.decode()
assert 'Mathematische Logik II' in r.data.decode()
r = self.app.get('/search?q=Afi+Stens')
assert r.status_code == 200
assert 'Analysis für Informatiker' in r.data.decode() and 'Höhere Mathematik I' in r.data.decode()
......@@ -177,6 +182,12 @@ class VideoTestCase(unittest.TestCase):
assert r.status_code == 200
assert 'Testtitle' in r.data.decode() and 'lectures.7353.title' in r.data.decode()
r = c.post('/internal/set/responsible/20/1', data={'_csrf_token': 'asd'})
assert r.status_code == 200
r = c.post('/internal/unset/responsible/20/1', data={'_csrf_token': 'asd'})
assert r.status_code == 200
def test_legacyurl(self):
with self.app as c:
......@@ -233,8 +244,8 @@ class VideoTestCase(unittest.TestCase):
r = self.app.post('/internal/jobs/api/worker/test/schedule', data=json.dumps({'jobtypes': ['thumbnail'], 'queues': ['default'], 'apikey': '1'}), content_type='application/json')
assert r.status_code == 200
jobdata = json.loads(json.loads(r.data.decode())['data'])
assert jobdata.get('lectureid') == '6981'
assert jobdata.get('path') == 'pub/hls/15ws-afi.m3u8'
assert jobdata.get('filename') == 'l_6981.jpg'
assert jobdata.get('src') == 'pub/hls/15ws-afi.m3u8'
r = self.app.get('/internal/streaming/legacy_auth/testserver', data={'app': 'live', 'call': 'publish_done', 'pass': 'caisoh8aht0wuSu', 'lecture': 6981, 'name': '15ws-afi'}, headers={'X-Real-IP': '137.226.35.193'})
assert r.status_code == 200
......@@ -260,14 +271,27 @@ class VideoTestCase(unittest.TestCase):
assert len(match) == 1
assert match[0]['id'] == 6095
# @unittest.skip("too slow")
def test_campusimport(self):
with self.app as c:
self.login(c)
r = self.app.get('/internal/sort/log')
assert r.status_code == 200
r = self.app.post('/internal/import/257', data={'campus.new.url': 'https://www.campus.rwth-aachen.de/rwth/all/event.asp?gguid=0x4664DBD60E5A02479B53089BF0EB0681&tguid=0x0B473CF286B45B4984CD02565C07D6F8', 'campus.new.type': 'Vorlesung'})
r = self.app.post('/internal/jobs/add/thumbnail', data={'lectureid': 10, '_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.get('/internal/sort/now')
assert r.status_code == 200
r = self.app.get('/internal/sort/encoded/something-wrong', data={'apikey': '1'})
assert r.status_code == 400
r = self.app.get('/internal/sort/encoded/09ss-dsal-090619-720p.mp4', data={'apikey': '1'})
assert r.status_code == 200
@unittest.skip("too slow")
def test_campusimport(self):
with self.app as c:
self.login(c)
r = self.app.post('/internal/import/257', data={'campus.new.url': 'https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbLv.wbShowLVDetail?pStpSpNr=269474&pSpracheNr=1', 'campus.new.type': 'Übung'})
assert r.status_code == 200
......@@ -279,3 +303,140 @@ class VideoTestCase(unittest.TestCase):
self.login(c)
r = self.app.get('/internal/cutprogress')
assert r.status_code == 200
# Some quick tests below to execute more sql statements
def test_encoding(self):
with self.app as c:
self.login(c)
r = self.app.post('/internal/jobs/add/remux', data={'videoid': 28, '_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.post('/internal/jobs/add/reencode', data={'videoid': 16080, '_csrf_token': 'asd'})
assert r.status_code == 302
# Trigger edit handler
r = c.get('internal/edit', data={"courses.3.title":"Test","_csrf_token":"asd"})
assert r.status_code == 200
def test_feeds(self):
with self.app as c:
r = self.app.get('/feed')
assert r.status_code == 200
r = self.app.get('/07ws-buk/feed')
assert r.status_code == 200
r = self.app.get('/07ws-buk/rss', data={'format_id':10})
assert r.status_code == 200
r = self.app.get('/courses/feed')
assert r.status_code == 200
def test_icalexport(self):
with self.app as c:
self.login(c)
r = self.app.get('/internal/ical/user/1')
assert r.status_code == 200
r = self.app.get('/internal/ical/notuser/1')
assert r.status_code == 200
r = self.app.get('/internal/ical/course/1')
assert r.status_code == 200
def test_jobs(self):
with self.app as c:
self.login(c)
r = self.app.get('/internal/jobs/overview')
assert r.status_code == 200
r = self.app.get('/internal/jobs/overview?worker=worker0')
assert r.status_code == 200
r = self.app.post('/internal/jobs/action/clear_failed', data={'_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.post('/internal/jobs/action/clear_failed/1', data={'_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.post('/internal/jobs/action/retry_failed', data={'_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.post('/internal/jobs/action/retry_failed/1', data={'_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.post('/internal/jobs/action/copy/1', data={'_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.post('/internal/jobs/action/delete/1', data={'_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.post('/internal/jobs/action/cancel/1', data={'_csrf_token': 'asd'})
assert r.status_code == 302
r = self.app.post('/internal/jobs/api/job/1/ping', data={'apikey': '1', 'host': 'test', 'status': '{}', 'state': 'finished'})
assert r.status_code == 205
# Test new worker
import uuid
r = self.app.post(f'/internal/jobs/api/worker/{uuid.uuid4()}/schedule', data=json.dumps({'jobtypes': ['probe'], 'queues': ['default'], 'apikey': '1'}), content_type='application/json')
assert r.status_code in [200, 503]
def test_auth(self):
with self.app as c:
r = self.app.get('/internal/auth', headers={'X-Original-Uri': 'https://videoag.fsmpi.rwth-aachen.de/files/pub/15ws-afi/15ws-afi-151022-720p.mp4'})
assert r.status_code == 200
# Not found, but sql is executed
r = self.app.get('/internal/auth', headers={'X-Original-Uri': 'https://videoag.fsmpi.rwth-aachen.de/files/pub/hls/something'})
assert r.status_code == 404
r = self.app.get('/internal/auth', headers={'X-Original-Uri': 'https://videoag.fsmpi.rwth-aachen.de/files/pub/hls/42'})
assert r.status_code == 404
def test_stats(self):
with self.app as c:
self.login(c)
r = self.app.get('/internal/stats')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/formats_views')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/course_count')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/lectures_count')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/categories_courses')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/organizer_courses')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/categories_lectures')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/lecture_views')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/live_views')
assert r.status_code == 200
r = self.app.get('/internal/stats/generic/lecture_totalviews')
assert r.status_code == 200
r = self.app.get('/internal/stats/viewsperday/lecture/1')
assert r.status_code == 200
r = self.app.get('/internal/stats/viewsperday/course/1')
assert r.status_code == 200
r = self.app.get('/internal/stats/viewsperday/global')
assert r.status_code == 200
r = self.app.get('/internal/stats/viewsperday/courses')
assert r.status_code == 200
from server import *
from datetime import time
@register_navbar('personalisierter Drehplan', icon='calendar', userendpoint=True, endpoint='timetable_user')
@register_navbar('Drehplan', icon='calendar')
@app.route('/internal/timetable')
@app.route('/internal/user/<int:user>/timetable', endpoint='timetable_user')
@mod_required
def timetable(user=None):
if 'kw' not in request.args:
if 'date' in request.args:
thisweekmonday = datetime.now()
thisweekmonday -= timedelta(days=thisweekmonday.weekday())
def get_monday(day):
return day-timedelta(days=day.weekday())
def get_week_offset(value):
if value is None:
return 0
day = None
for pattern in ['%d-%m-%Y-1', '%Y-W%W-%w']:
try:
datesweekmonday = datetime.strptime(request.args['date'], '%d-%m-%Y')
except ValueError:
datesweekmonday = None
if not datesweekmonday:
try:
datesweekmonday = datetime.strptime(request.args['date'] + '-1', "%Y-W%W-%w")
day = datetime.strptime(value+'-1', pattern)
except ValueError:
datesweekmonday = None
pass
if day is not None:
break
if day is None:
return 0
return int((get_monday(day) - get_monday(datetime.now())).days/7)
if not datesweekmonday:
kw = 0
else:
datesweekmonday -= timedelta(days=datesweekmonday.weekday())
kw = int((datesweekmonday.date() - thisweekmonday.date()).days/7)
else:
kw=0
else:
kw=int(request.args['kw'])
try:
start = date.today() - timedelta(days=date.today().weekday() -7*kw)
except:
start = date.today() - timedelta(days=date.today().weekday())
weekofyear = '{}-W{:02d}'.format(datetime.today().year, datetime.today().isocalendar()[1])
days = [{'date': start, 'lectures': [], 'atonce':0, 'index': 0 }]
earlieststart=time(23,59)
latestend=time(0,0)
for i in range(1,7):
days.append({'date': days[i-1]['date'] + timedelta(days=1), 'atonce':0, 'index': i, 'lectures':[] })
for i in days:
# date and times are burning in sqlite
s = datetime.combine(i['date'],time(0,0))
e = datetime.combine(i['date'],time(23,59))
i['lectures'] = []
for l in query ('''
SELECT lectures.*, courses.short, "course" AS sep, courses.*
def query_lectures_on_day(start, end):
# What we want to match:
# lecture.time <= end AND lecture.time+lecture.duration >= start
# But there is no SQL statement that does this and is compatible with both sqlite
# and mysql, so we approximate the "lecture.time+lecture.duration" part
rows = query('''SELECT lectures.*, courses.short, \'course\' AS sep, courses.*
FROM lectures
JOIN courses ON (lectures.course_id = courses.id)
WHERE time < ? AND time > ? AND NOT norecording AND NOT external
ORDER BY time ASC''', i['date']+timedelta(weeks=2), i['date']-timedelta(weeks=2)):
# we can not use the where clause of sql to match against the time, because sqlite and mysql use a different syntax -.-
# we still use it to only get the lectures for a 3 week periode
if not l['time']:
l['time'] = datetime.fromtimestamp(0)
if ((l['time'] < e) and (l['time'] > s)) or ((l['time'] + timedelta(minutes=l['duration']) < e) and (l['time'] + timedelta(minutes=l['duration'])> s)):
# filter on responsible user if a user parameter was given
l['responsible'] = query('''SELECT users.*
FROM responsible
JOIN users ON (responsible.user_id = users.id AND responsible.course_id = ?)
ORDER BY users.realname ASC''', l['course_id'])
if len(l['responsible']) == 0:
l['responsible'] = [{"realname": "Niemand", "id": -1}]
if not user or user in [ r['id'] for r in l['responsible'] ]:
i['lectures'].append(l)
WHERE time <= ? AND time > ?
ORDER BY time ASC''', end, start-timedelta(weeks=2))
lectures = []
for lecture in rows:
if lecture['time']+timedelta(minutes=lecture['duration']) >= start:
lectures.append(lecture)
return lectures
oldtime = l['time']
l['time'] = max(s,l['time'])
l['duration'] = ( min(e,oldtime + timedelta(minutes=l['duration'])) - l['time'] ).total_seconds()/60
# sweepline to find out how many lectures overlap
maxcol=0;
curcol=0;
freecol=[];
for l in i['lectures']:
# who the hell inserts lectures with zero length?!?!?
l['time_end'] = l['time']+timedelta(minutes=max(l['duration'],1))
# create sweepline input array
sweeplinetupels = [(l['time'],True,l) for l in i['lectures']]
sweeplinetupels += [(l['time_end'],False,l) for l in i['lectures']]
tmp = []
for x in sweeplinetupels:
unique = True
for y in tmp:
if x[0] == y[0] and x[1] == y[1] and x[2]['short'] == y[2]['short']:
unique = False
if unique:
tmp.append(x)
'''Use a sweepline algorithm to find overlapping lectures
sweeplinetupels = sorted(tmp, key=lambda t:(t[0],t[1]))
for l in sweeplinetupels:
if l[1]:
For each day the item 'maxcol' will be the number of columns required to display
the overlapping lectures. For each lecture the item 'timetable_col' will be the
index of the column the lecture is going to be rendered in.'''
def timetable_sweepline(days):
earliest_start = time(23, 59)
latest_end = time(0, 0)
for day in days:
sweeplinetupels = [(lecture['time'].time(), True, lecture) for lecture in day['lectures']]
sweeplinetupels += [(lecture['time_end'].time(), False, lecture) for lecture in day['lectures']]
maxcol = 0
curcol = 0
freecol = []
sweeplinetupels.sort(key=lambda row: row[:2])
for timestamp, is_start, lecture in sweeplinetupels:
if is_start:
curcol += 1
if curcol > maxcol:
maxcol = curcol
if len(freecol) == 0:
freecol.append(maxcol)
l[2]['timetable_col'] = freecol.pop()
if earlieststart > l[0].time():
earlieststart = l[0].time()
maxcol = max(maxcol, curcol)
if freecol:
lecture['timetable_col'] = freecol.pop()
else:
lecture['timetable_col'] = maxcol
earliest_start = min(earliest_start, timestamp)
else:
curcol -= 1
freecol.append(l[2]['timetable_col'])
if latestend < l[0].time():
latestend = l[0].time()
i['maxcol'] = max(maxcol,1)
times=[]
s = min(earlieststart,time(8,0))
e = max(latestend,time(19,0))
for i in range(s.hour*4,min(int((60*e.hour/15)/4)*4+5,24*4)):
t = i*15
times.append(time(int(t/60),t%60))
return render_template('timetable.html',days=days,times=times,kw=kw, weekofyear=weekofyear, user=query('SELECT * FROM users WHERE id = ?', user)[0] if user else None)
freecol.append(lecture['timetable_col'])
latest_end = max(latest_end, timestamp)
day['maxcol'] = max(maxcol, 1)
return earliest_start, latest_end
@register_navbar('personalisierter Drehplan', icon='calendar', userendpoint=True, endpoint='timetable_user')
@register_navbar('Drehplan', icon='calendar')
@app.route('/internal/timetable')
@app.route('/internal/user/<int:user>/timetable', endpoint='timetable_user')
@mod_required
def timetable(user=None):
if 'kw' in request.args:
week_offset = int(request.args['kw'])
else:
week_offset = get_week_offset(request.args.get('date', None))
start_day = date.today() - timedelta(days=date.today().weekday() - 7*week_offset)
days = [{'date': start_day, 'lectures': [], 'atonce': 0, 'index': 0}]
for i in range(1, 7):
days.append({'date': days[i-1]['date'] + timedelta(days=1), 'atonce': 0, 'index': i, 'lectures': []})
for day in days:
start = datetime.combine(day['date'], time(0, 0))
end = datetime.combine(day['date'], time(23, 59))
day['lectures'] = []
for lecture in query_lectures_on_day(start, end):
lecture['time_end'] = lecture['time']+timedelta(minutes=lecture['duration'])
# "Crop" lecture's timespan to start/end of day
lecture['time'] = max(start, lecture['time'])
lecture['time_end'] = min(end, lecture['time_end'])
# Ensure length > 0
lecture['time_end'] = max(lecture['time_end'], lecture['time']+timedelta(minutes=1))
lecture['duration'] = int((lecture['time_end'] - lecture['time']).total_seconds()/60)
# Filter on responsible user if a user parameter was given
lecture['responsible'] = query('''SELECT users.*
FROM responsible
JOIN users ON (responsible.user_id = users.id AND responsible.course_id = ?)
ORDER BY users.realname ASC''', lecture['course_id'])
if len(lecture['responsible']) == 0:
lecture['responsible'] = [{"realname": "Niemand", "id": -1}]
if not user or user in [r['id'] for r in lecture['responsible']]:
day['lectures'].append(lecture)
earliest_start, latest_end = timetable_sweepline(days)
start = min(earliest_start, time(8, 0))
end = max(latest_end, time(19, 0))
blocks = []
for i in range(start.hour*4, min(int((60*end.hour/15)/4)*4+5, 24*4)):
timestamp = i*15
blocks.append(time(int(timestamp/60), timestamp%60))
weekofyear = '{}-W{:02d}'.format(datetime.today().year, datetime.today().isocalendar()[1])
return render_template('timetable.html',
days=days,
blocks=blocks,
kw=week_offset,
weekofyear=weekofyear,
user=query('SELECT * FROM users WHERE id = ?', user)[0] if user else None)