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
  • intros
  • live_sources
  • master
  • modules
5 results
Show changes
Showing
with 864 additions and 325 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
......@@ -16,7 +16,7 @@
<link href="{{url_for('static', filename='bootstrap/bootstrap.css')}}" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='style.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='font-awesome/css/font-awesome.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='font-awesome/css/all.css')}}">
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='videojs/video-js.css')}}">
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='videojs/videojs.markers.css')}}">
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='videojs/videojs-resolution-switcher.css')}}">
......@@ -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>
......@@ -87,7 +88,7 @@
<h1 class="panel-title">Videos
{% if ismod() %}
<a class="btn btn-default" style="margin-right: 5px;" href="{{ url_for('create', table='lectures', time=datetime.now(), title='Noch kein Titel', visible='0', course_id=course.id, ref=url_for('course', id=course.id)) }}">Neuer Termin</a>
<a class="btn btn-default" style="margin-right: 5px;" href="{{url_for('list_import_sources', id=course['id'])}}">Campus Import</a>
<a class="btn btn-default" style="margin-right: 5px;" href="{{url_for('list_import_sources', id=course['id'])}}">Import</a>
{% endif %}
<ul class="list-inline pull-right">
<li>
......@@ -107,6 +108,7 @@
</ul>
</div>
{% if ismod() %}
<div class="modal fade" id="editperm" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
......@@ -124,34 +126,161 @@
<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>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="editstream" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Livestream einrichten</h4>
</div>
<div class="modal-body">
<form id="editstream-form">
<div class="row" style="padding-left: 10px; padding-right: 10px;" data-id="-1" data-type="-1">
{% for snum in [1,2] %}
<div class="col-xs-6">
<label>Quelle {{ snum }}</label>
<select name="source{{ snum }}" class="form-control source-select">
<option value="">Nichts</option>
{% for source in live_sources %}
<option value="{{ source.id }}">{{ source.name }}</option>
{% endfor %}
</select>
<img src="{{ url_for('static', filename='smptebars.jpg') }}" style="width: 100%; margin-bottom: 0.5em; margin-top: 0.5em"/>
<div class="checkbox"><label><input name="source{{ snum }}_deinterlace" type="checkbox">Video deinterlacen</label></div>
<label>Lautstärke</label>
<div class="row">
<div class="col-xs-6">
<input type="range" name="source{{ snum }}_leftvolume" value="100">
</div>
<div class="col-xs-6">
<input type="range" name="source{{ snum }}_rightvolume" value="100">
</div>
</div>
<select name="source{{ snum }}_audiomode" class="form-control" style="margin-top: 0.5em">
<option value="mono" selected>Mono-Mix</option>
<option value="stereo">Stereo</option>
<option value="unchanged">Audio unverändert</option>
<option value="off">Kein Audio</option>
</select>
</div>
{% endfor %}
<div class="col-xs-12" style="margin-top: 1em">
<label>Video</label>
<select name="videomode" class="form-control">
<option value="1" selected>Nur Quelle 1</option>
<option value="2">Nur Quelle 2</option>
<option value="lecture4:3">Quelle 1 (4:3) links, Ausschnitt von 2 rechts</option>
<option value="lecture16:9">Quelle 1 (16:9) links, Ausschnitt von 2 rechts</option>
<option value="sidebyside">Side-by-Side (Quelle 1 links, 2 rechts)</option>
</select>
<div class="checkbox"><label><input name="video_showlogo" type="checkbox" checked>Video AG-Logo einblenden</label></div>
</div>
<div class="col-xs-12" style="margin-top: 1em">
<label>Weitere Einstellungen</label>
<div style="margin-top: -1em;">
<div class="checkbox"><label><input name="audio_normalize" type="checkbox">Lautstärke normalisieren</label></div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<form class="form-inline" method="post" action="{{ url_for('control_stream') }}">
<input type="hidden" id="editstream-lectureid" name="lecture_id" value="">
<input type="hidden" id="editstream-action" name="action" value="">
<button type="submit" id="editstream-start" class="btn btn-danger">Speichern und starten</button>
<button type="button" id="editstream-submit" class="btn btn-primary">Speichern</button>
</form>
</div>
</div>
</div>
</div>
<script>
{% if ismod() %}
function editstream_update() {
$('#editstream .source-select').each(function () {
if ($(this).val())
$(this).siblings('img').prop('src', '{{ config.VIDEOPREFIX }}/thumbnail/s_'+$(this).val()+'.jpg');
else
$(this).siblings('img').prop('src', {{ url_for('static', filename='smptebars.jpg')|tojson }});
});
};
function editstream_dump() {
var res = {};
$("#editstream input:checkbox[name!='']").each(function () {
res[$(this).attr('name')] = $(this).prop('checked');
});
$("#editstream select[name!='']").each(function () {
res[$(this).attr('name')] = $(this).val();
});
$("#editstream input[type='range'][name!='']").each(function () {
res[$(this).attr('name')] = $(this).val();
});
return res;
};
function editstream_load(obj) {
$("#editstream input:checkbox[name!='']").each(function () {
if ($(this).attr('name') in obj)
$(this).prop('checked', obj[$(this).attr('name')]);
});
$("#editstream select[name!='']").each(function () {
if ($(this).attr('name') in obj)
$(this).val(obj[$(this).attr('name')]);
});
$("#editstream input[type='range'][name!='']").each(function () {
if ($(this).attr('name') in obj)
$(this).val(obj[$(this).attr('name')]);
});
};
$('#editstream .source-select').on('change', editstream_update);
$('#editstream-submit').on('click', function () {
moderator.api.set($('#editstream').data('currentpath'), JSON.stringify(editstream_dump()), true);
$('#editstream').modal('hide');
});
$('#editstream-start').on('click', function () {
moderator.api.set($('#editstream').data('currentpath'), JSON.stringify(editstream_dump()), true);
return true;
});
$('#editstream').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
$('#editstream').data('currentpath', button.data('path'));
if (button.data('active')) {
$('#editstream-action').val('stop');
$('#editstream-start').text('Stream stoppen');
} else {
$('#editstream-action').val('start');
$('#editstream-start').text('Speichern und starten');
}
$('#editstream-lectureid').val(button.data('lectureid'));
$("#editstream-form")[0].reset();
if (button.data('value'))
editstream_load(button.data('value'));
editstream_update();
});
$('#responsible-select').multiselect({enableCaseInsensitiveFiltering: true,
maxHeight: 200, numberDisplayed: 5, nonSelectedText: 'Niemand',
nSelectedText: 'ausgewählt', allSelectedText: false,
onChange: function(option, checked, select) {
moderator.api.set_map('responsible', {{ course.id }}, option.val(), checked);
}})
{% endif %}
$.ajax({
method: "GET",
url: "{{url_for('stats_generic', req="lecture_views", param=course.id)}}",
......
......@@ -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 %}
......@@ -51,12 +51,12 @@
<p>Auf Wunsch der Dozenten schränken wir den Zugriff auf Videos vieler Veranstaltungen ein. In der Regel ist dies eine Vorraussetzung dafür, dass wir eine Vorlesung überhaupt aufnehmen dürfen.</p>
<p>Wer auf ein bestimmtes Videos zugreifen kann, wird auf der Veranstaltungsseite durch Symbole auf der rechten Seite angezeigt. Dabei gibt es folgende Möglichkeiten:</p>
<ul>
<li><strong>Öffentliche</strong> Videos (<span class="fa fa-globe"></span>) können von jedem abgespielt werden.</li>
<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>
......
......@@ -4,11 +4,11 @@
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title">Campus Import für <strong>{{course.title}}</strong> <span><a href="{{url_for('course', handle=course.handle)}}" class="btn btn-default" >Zur Veranstaltungsseite</a><span> </h1>
<h1 class="panel-title">Campus-/RWTHonline-Import für <strong>{{course.title}}</strong> <span><a href="{{url_for('course', handle=course.handle)}}" class="btn btn-default" >Zur Veranstaltungsseite</a><span> </h1>
</div>
<div class="panel-body">
<div>
<p>Es folgen viele Pärchen an Campus-URL und Veranstaltungstyp Pärchen. Die Campus URL bekommt man aus dem Campus-System (<a href="https://www.campus.rwth-aachen.de/rwth/all/groups.asp" target="_blank">hier</a>). Der Veranstaltungstyp ist z.B. "Vorlesung" oder "Übung" oder "Praktikum".
<p>Es folgen ein oder mehrere Veranstaltungs-URLs mit dem jeweiligen Veranstaltungstyp. Die Veranstaltungs-URL bekommt man aus dem Campus-System (<a href="https://www.campus.rwth-aachen.de/rwth/all/groups.asp" target="_blank">hier</a>) bzw. bei RWTHonline über die Veranstaltungssuche (<a href="https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbSuche.LVSuche" target="_blank">hier</a> oder über Durchklicken im neuen Interface). Der Veranstaltungstyp ist z.B. "Vorlesung" oder "Übung" oder "Praktikum".
</p>
<form method="post" action="{{url_for('list_import_sources', id=course['id'])}}">
<ul class="list-group row" style="margin-left: 0px; margin-right: 0px;">
......@@ -73,7 +73,7 @@
<span class="col-xs-3">
</span>
<span class="pull-right">
<button class="btn btn-default newlecture" onclick="moderator.api.gethttp('{{ url_for('create', table='lectures', course_id=course.id, time=i.time, title=i.title, place=i.place) }}')">anlegen</a>
<button class="btn btn-default newlecture" onclick="moderator.api.gethttp('{{ url_for('create', table='lectures', course_id=course.id, time=i.time, title=i.title, place=i.place, duration=i.duration) }}')">anlegen</a>
</span>
</li>
{% endfor %}
......
{% 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>
......
......@@ -13,9 +13,11 @@
</div>
<div class="modal-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#add_thumbnail" aria-controls="thumbnail" role="tab" data-toggle="tab">Thumbnail</a></li>
<li role="presentation"><a href="#add_remux" aria-controls="remux" role="tab" data-toggle="tab">Remux</a></li>
<li role="presentation"><a href="#add_reencode" aria-controls="remux" role="tab" data-toggle="tab">Reencode</a></li>
<li role="presentation" class="active"><a href="#add_thumbnail" role="tab" data-toggle="tab">Thumbnail</a></li>
<li role="presentation"><a href="#add_remux" role="tab" data-toggle="tab">Remux</a></li>
<li role="presentation"><a href="#add_reencode" role="tab" data-toggle="tab">Reencode</a></li>
<li role="presentation"><a href="#add_stream_forward" role="tab" data-toggle="tab">Stream-Weiterleitung</a></li>
<li role="presentation"><a href="#add_forward" role="tab" data-toggle="tab">RTMP-Weiterleitung</a></li>
</ul>
<div class="tab-content" style="margin-top: 10px;">
<div role="tabpanel" class="tab-pane active" id="add_thumbnail">
......@@ -48,6 +50,31 @@
</div>
</form>
</div>
<div role="tabpanel" class="tab-pane" id="add_forward">
<form class="form-inline" action="{{url_for('add_forward_job', ref=request.url)}}" method="post">
<div class="form-group">
<input type="text" class="form-control" placeholder="Quelle" name="src">
<input type="text" class="form-control" placeholder="Ziel" name="dest">
<button type="submit" class="btn btn-primary">Livestream weiterleiten</button>
</div>
</form>
</div>
<div role="tabpanel" class="tab-pane" id="add_stream_forward">
<form class="form-inline" action="{{url_for('add_forward_job', ref=request.url)}}" method="post">
<div class="form-group">
<select name="src" class="form-control">
{% for lecture in active_streams %}
<option value="{{ lecture.destbase }}_low">{{ lecture.course.short }}: {{ lecture.title }} (360p)</option>
<option value="{{ lecture.destbase }}_mid">{{ lecture.course.short }}: {{ lecture.title }} (720p)</option>
<option value="{{ lecture.destbase }}_high">{{ lecture.course.short }}: {{ lecture.title }} (1080p)</option>
{% endfor %}
</select>
<input type="text" class="form-control" placeholder="Ziel" name="dest">
<button type="submit" class="btn btn-primary">Livestream weiterleiten</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
......@@ -204,7 +231,7 @@
{% if i.state == "failed" %}
<li>
<a class="btn btn-default" href="{{url_for('jobs_action', action='retry_failed', jobid=i.id, ref=request.url)}}" title="Neustarten">
<span class="fa fa-refresh" aria-hidden="true"></span>
<span class="fa fa-sync" aria-hidden="true"></span>
</a>
</li>
<li>
......@@ -215,7 +242,7 @@
{% else %}
<li>
<a class="btn btn-default" href="{{url_for('jobs_action', action='copy', jobid=i.id, ref=request.url)}}" title="Kopie neu einreihen">
<span class="fa fa-refresh" aria-hidden="true"></span>
<span class="fa fa-sync" aria-hidden="true"></span>
</a>
</li>
{% if i.state in ["failed", "ready"] %}
......
{% 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 method="post" data-url="{{ url_for('suggest_chapter', lectureid=lecture.id) }}" onSubmit="return hintchapterclick(this);"><input class="form-control" placeholder="00:00.000" name="time" type="text" value="'+timeasstring+'"><br><input class="form-control" placeholder="Kapitel" name="text" type="text"><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>'
}
)
......@@ -212,7 +299,7 @@ $('#embedcodebtn').popover(
<div class="row">
{% if ismod() or (videos|length > 0) %}
<div style="background-image: url('{{ config.VIDEOPREFIX }}/thumbnail/l_{{lecture.id}}.jpg')" class="col-sm-2 col-xs-12 thumbnailimg">
{% if not videos|length is equalto 0 %}
{% if (not videos|length is equalto 0) or ismod() %}
<a href="{{url_for('lecture', course=lecture.course.handle, id=lecture.id)}}">
<span class="glyphicon glyphicon-play-circle playpreviewbtn"></span>
</a>
......@@ -260,6 +347,13 @@ $('#embedcodebtn').popover(
<li class="pull-right">
{{ moderator_delete(['lectures',lecture.id,'deleted']) }}
</li>
{% if ismod() %}
<li class="pull-right">
<button class="btn btn-{{ 'default' if not lecture.stream_settings else 'danger' if lecture.stream_job else 'primary' }}" data-toggle="modal" data-target="#editstream" data-lectureid="{{ lecture.id }}" data-active="{{ 1 if lecture.stream_job else '' }}" data-path="{{ 'lectures.%i.stream_settings'%lecture.id }}" data-value='{{ lecture.stream_settings|e }}'>
<span class="fas fa-broadcast-tower"></span>
</button>
</li>
{% endif %}
</ul>
</li>
{% if ismod() %}
......@@ -328,7 +422,7 @@ $('#embedcodebtn').popover(
{% set permlogos = '' %}
{% if permdescription[0] == 'public' %}
{% set permlogos = '<span class="fa fa-globe" aria-hidden="true"></span>' %}
{% set permlogos = '<span class="fa fa-globe-asia" aria-hidden="true"></span>' %}
{% endif %}
{% if permdescription[0] == 'none' %}
{% set permlogos = '<span class="fa fa-ban" aria-hidden="true"></span>' %}
......@@ -339,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>
......
{% extends "base.html" %}
{% block content %}
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title">Streamquellen</h1>
</div>
<div class="panel-body">
<form action="{{ url_for('create', table='live_sources', ref=request.url) }}" method="post">
<div class="input-group pull-right" style="width: 300px;">
<input type="text" class="form-control" name="name" placeholder="Quellenname" width="100px">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Quelle anlegen</button>
</span>
</div>
</form>
</div>
<ul class="list-group">
{% for source in sources %}
<li class="list-group-item{% if source.clientid %} list-group-item-danger{% endif %}">
<div class="row">
<div style="background-image: url('{{ config.VIDEOPREFIX }}/thumbnail/s_{{ source.id }}.jpg')" class="col-sm-2 col-xs-12 thumbnailimg">
{% if source.clientid %}
<a href="#" data-toggle="modal" data-target="#preview-player" data-srcname="{{ source.name }}" data-srckey="{{ source.key }}">
<span class="glyphicon glyphicon-play-circle playpreviewbtn"></span>
</a>
{% endif %}
</div>
<ul class="list-unstyled col-sm-3 col-xs-12">
<li>{{ moderator_editor(['live_sources',source.id,'name'], source.name) }}</li>
<li>{{ moderator_editor(['live_sources',source.id,'description'], source.description) }}</li>
</ul>
<ul class="list-unstyled col-sm-3 col-xs-12">
{% if source.clientid %}
<li><a href="rtmp://{{ source.server_public }}/src/{{ source.id }}?preview_key={{ source.preview_key }}">rtmp://{{ source.server_public }}/src/{{ source.id }}</a></li>
{% if source.stat and source.video and source.audio %}
<li>Quelladresse: {{ source.stat.address }}</li>
<li>Framedrops: {{ source.stat.dropped }}</li>
<li>Auflösung: {{ source.video.width }}x{{ source.video.height }}</li>
<li>Framerate: {{ source.video.frame_rate }} fps</li>
<li>Audiokanäle: {{ source.audio.channels }}</li>
<li>Audiorate: {{ source.audio.sample_rate }} Hz</li>
<li>Codecs: {{ source.video.codec }}/{{ source.audio.codec }}</li>
{% endif %}
{% else %}
{% if source.last_active %}
<li>Zuletzt aktiv: {{ source.last_active|date }}, {{ source.last_active|time }} Uhr</li>
{% else %}
<li>Noch nie aktiv</li>
{% endif %}
{% endif %}
</ul>
<ul class="list-unstyled col-sm-4 col-xs-12">
<li class="pull-right">{{ moderator_delete(['live_sources',source.id,'deleted']) }}</li>
<a href="{{ url_for('streamrekey', id=source.id) }}" class="btn btn-{{ 'primary' if not source.key else 'default' }}">Streamkey erneuern</a>
<a href="{{ url_for('streamdrop', id=source.id, ref=request.url) }}" class="btn btn-danger {% if not source.clientid %} disabled{% endif %}">Trennen</a>
</ul>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
<div id="preview-player" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Vorschau von</h4>
</div>
<div class="modal-body">
</div>
</div>
</div>
</div>
<script>
$('#preview-player').on('show.bs.modal', function (e) {
var btn = $(e.relatedTarget);
$(this).find('.modal-title').text('Vorschau von '+btn.data('srcname'));
$(this).find('.modal-body').html('<video id="previewplayer" style="width: 100%" class="video-js vjs-default-skin vjs-big-play-centered" width="640" height="320" controls><source type="application/x-mpegURL" src="{{config.VIDEOPREFIX}}/pub/hls/preview/'+btn.data('srckey')+'.m3u8"/></video>');
var player = videojs('previewplayer');
player.play();
});
$('#preview-player').on('hidden.bs.modal', function (e) {
videojs('previewplayer').dispose();
$(this).find('.modal-body').text('');
});
</script>
{% endblock %}
......@@ -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 %}
......