Commit 6dd57121 authored by Julian Rother's avatar Julian Rother
Browse files

Merge branch 'master' into 295-ldap-authentifizierung-an-active-directory

parents b0a0545a 1399eed3
......@@ -29,3 +29,4 @@ LDAP_GROUPS = ['users']
#ICAL_URL = 'https://user:password@mail.fsmpi.rwth-aachen.de/SOGo/....ics'
ERROR_PAGE = 'static/500.html'
RWTH_IP_RANGES = ['134.130.0.0/16', '137.226.0.0/16', '134.61.0.0/16', '192.35.229.0/24', '2a00:8a60::/32']
FSMPI_IP_RANGES = ['137.226.35.192/29', '137.226.75.0/27', '137.226.127.32/27', '137.226.231.192/26', '134.130.102.0/26' ]
......@@ -13,7 +13,7 @@ if config['DB_ENGINE'] == 'sqlite':
hours, minutes, seconds = map(int, timepart_full[0].split(b":"))
val = datetime(year, month, day, hours, minutes, seconds, 0)
except ValueError:
val = None
val = datetime.fromtimestamp(0)
return val
sqlite3.register_converter('datetime', convert_timestamp)
......
......@@ -14,19 +14,19 @@ editable_tables = {
'idcolumn': 'id',
'editable_fields': {
'visible': {'type': 'boolean'},
'listed': {'type': 'boolean'},
'listed': {'type': 'boolean', 'description': 'Soll die Veranstaltung auf der Hauptseite gelistet werden?'},
'title': {'type': 'shortstring'},
'short': {'type': 'shortstring'},
'short': {'type': 'shortstring', 'description': 'Abkürzung für die Veranstaltung, z.B. für den Drehplan'},
'handle': {'type': 'shortstring'},
'organizer': {'type': 'shortstring'},
'subject': {'type': 'shortstring'},
'semester': {'type': 'shortstring'},
'downloadable': {'type': 'boolean'},
'downloadable': {'type': 'boolean', 'description': 'Hiermit kann der Download-Button disabled werden'},
'internal': {'type': 'text'},
'responsible': {'type': 'shortstring'},
'deleted': {'type': 'boolean'},
'description': {'type': 'text'},
'external': {'type': 'boolean'}},
'external': {'type': 'boolean', 'description': 'Soll die Veranstaltung nicht im Drehplan angezeigt werden?'}},
'creationtime_fields': ['created_by', 'time_created', 'time_updated'] },
'lectures': {
'table': 'lectures_data',
......@@ -42,7 +42,7 @@ editable_tables = {
'duration': {'type': 'duration'},
'jumplist': {'type': ''},
'deleted': {'type': 'boolean'},
'live': {'type': 'boolean'},
'live': {'type': 'boolean', 'description': 'Ist ein Livestream geplant?'},
'norecording': {'type': 'boolean'}},
'creationtime_fields': ['course_id', 'time_created', 'time_updated'] },
'videos': {
......@@ -110,6 +110,13 @@ def parseeditpath(path):
assert column in editable_tables[table]['editable_fields']
type = editable_tables[table]['editable_fields'][column]['type']
return {'table': table, 'id': id, 'column': column, 'type': type, 'tableinfo': editable_tables[table]}
@app.template_filter(name='getfielddescription')
def getfielddescription(path):
p = parseeditpath(path)
desc = p['tableinfo']['editable_fields'][p['column']].get('description', '')
if desc != '':
desc = '<br>'+desc
return desc
@app.route('/internal/edit', methods=['GET', 'POST'])
@mod_required
......
from server import *
import icalendar
from werkzeug.datastructures import Headers
from datetime import timedelta, datetime
def export_lectures(lectures, name):
cal = icalendar.Calendar()
cal.add('prodid', '-//Video AG//rwth.video//')
cal.add('version', '1.0')
for l in lectures:
event = icalendar.Event()
event.add('summary', l['course']['short']+': '+l['title'])
event.add('description', '\n\n'.join([s for s in [
l['comment'],
l['internal'],
'Zuständig: '+l['course']['responsible'] if l['course']['responsible'] else ''
] if s]))
event.add('uid', '%i@rwth.video'%l['id'])
event.add('dtstamp', datetime.utcnow())
event.add('categories', l['course']['short'])
event.add('dtstart', l['time'])
event.add('location', l['place'])
event.add('dtend', l['time'] + timedelta(minutes=l['duration']))
cal.add_component(event)
h = Headers()
h.add_header("Content-Disposition", "inline", filename=name)
return Response(cal.to_ical(), mimetype="text/calendar", headers=h)
def calperm(func):
@wraps(func)
def decorator(*args, **kwargs):
permission = ismod()
if 'X-Real-IP' in request.headers:
ip = ip_address(request.headers['X-Real-IP'])
for net in config['FSMPI_IP_RANGES']:
if ip in ip_network(net):
permission = True
if permission:
return func(*args, **kwargs)
else:
flash('Diese Funktion ist nur aus dem FSMPI-Netz(für SOGO-Import) oder eingeloggt verfügbar!')
return redirect(url_for('index'))
return decorator
@app.route('/internal/ical/all')
@calperm
def ical_all():
return export_lectures(query('''SELECT lectures.*, "course" AS sep, courses.*
FROM lectures JOIN courses ON courses.id = lectures.course_id
WHERE NOT norecording AND NOT external ORDER BY time DESC LIMIT 1000'''),'videoag_all.ics')
@app.route('/internal/ical/course/<course>')
@calperm
def ical_course(course):
return export_lectures(query('''SELECT lectures.*, "course" AS sep, courses.*
FROM lectures JOIN courses ON courses.id = lectures.course_id
WHERE courses.handle = ? AND NOT norecording AND NOT external ORDER BY time DESC''', course),'videoag_course_'+course+'.ics')
......@@ -404,7 +404,7 @@ def course(id=None, handle=None):
if perm['lecture_id'] == lecture['id']:
lecture['perm'].append(perm)
videos = query('''
SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, formats.description AS format_description, formats.player_prio, formats.prio
SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, "formats" AS sep, formats.*
FROM videos
JOIN lectures ON (videos.lecture_id = lectures.id)
JOIN formats ON (videos.video_format = formats.id)
......@@ -412,7 +412,7 @@ def course(id=None, handle=None):
WHERE lectures.course_id= ? AND (? OR videos.visible)
ORDER BY lectures.time, formats.prio DESC
''', course['id'], ismod())
livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, formats.description AS format_description, formats.player_prio, formats.prio
livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
FROM streams
JOIN lectures ON lectures.id = streams.lecture_id
JOIN formats ON formats.keywords = "hls"
......@@ -434,14 +434,14 @@ def faq():
def lecture(id, course=None, courseid=None):
lecture = query('SELECT * FROM lectures WHERE id = ? AND (? OR visible)', id, ismod())[0]
videos = query('''
SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, formats.description AS format_description, formats.player_prio, formats.prio, formats.mimetype
SELECT videos.*, (videos.downloadable AND courses.downloadable) as downloadable, "formats" AS sep, formats.*
FROM videos
JOIN formats ON (videos.video_format = formats.id)
JOIN courses ON (courses.id = ?)
WHERE videos.lecture_id = ? AND (? OR videos.visible)
ORDER BY formats.prio DESC
''', lecture['course_id'], lecture['id'], ismod())
livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, formats.description AS format_description, formats.player_prio, formats.prio, formats.mimetype
livestreams = query('''SELECT streams.handle AS livehandle, streams.lecture_id, "formats" AS sep, formats.*
FROM streams
JOIN lectures ON lectures.id = streams.lecture_id
JOIN formats ON formats.keywords = "hls"
......@@ -687,3 +687,4 @@ if 'JOBS_API_KEY' in config:
import jobs
import timetable
import chapters
import icalexport
......@@ -191,8 +191,11 @@ var moderator = {
html += '<option value="none">Kein Zugriff</option>';
html += '</select>';
html += '<input class="col-xs-12 passwordinput authuser" type="text" placeholder="Benutzername">';
html += '<input class="col-xs-12 passwordinput authpassword" type="text" placeholder="Passwort">';
html += '<input class="col-xs-10 passwordinput authpassword" type="text" placeholder="Passwort">'
html += '<button class="col-xs-2 passwordinput authpgen" type="button" onclick="$(\'.authpassword\',this.parentNode).val(moderator.permissioneditor.randompw());"><span class="fa fa-plus" aria-hidden="true"></span></button>'
html += '<input class="col-xs-12 authl2p" type="text" placeholder="Lernraum" style="display: none;">';
html += '<button class="col-xs-6" onclick="moderator.permissioneditor.addbtnclick(this)">Add</button>';
//html += '<button class="col-xs-4" onclick="moderator.permissionedior.updatebtnclick(this)">Update</button>';
html += '<button class="col-xs-6" onclick="moderator.permissioneditor.delbtnclick(this)">Delete</button>';
......@@ -248,58 +251,76 @@ var moderator = {
$(".authl2p",element.parentElement).hide();
break;
}
},
randompw: function () {;
var array = new Uint8Array(10);
window.crypto.getRandomValues(array);
var result = '';
for (var i = 0; i< array.length; i++) {
result += String.fromCharCode(48+ (array[i]/255.0)*74);
}
return result;
}
},
plots: {
init: function() {
$(window).on("resize", moderator.plots.resize);
$(".plotlyresize").on("click", moderator.plots.resize);
moderator.plots.createplots(".plot-view")
},
resize: function() {
$(".plot-view").each(function () {Plotly.Plots.resize(this)});
},
createplots: function (selector) {
var l = $(selector);
for (var i = 0; i < l.length; i ++) {
if (!l[i].id)
l[i].id = "plot-"+i;
$(l[i]).html('<div class="plot-loader"></div>');
$.ajax({
divobj: l[i],
method: "GET",
url: l[i].dataset.url,
dataType: "json",
error: function (jqXHR, textStatus, errorThrow) {
$(this.divobj).html('<div class="plot-error">'+errorThrow+'</div>');
},
success: function (traces) {
var layout = {margin: {l: 30, r: 30, t: 10, b: 70, pad: 0}};
for (var i = 0; i < traces.length; i ++) {
traces[i].type = this.divobj.dataset.type;
}
if (this.divobj.dataset.type == "pie")
layout.showlegend = false;
traces.sort(function (a, b) {
asum = 0;
bsum = 0;
for (var i = 0; i < a.y.length; i++)
asum += a.y[i]
for (var i = 0; i < b.y.length; i++)
bsum += b.y[i]
return bsum-asum;
});
for (var i = 0; i < traces.length; i++) {
if (i > 15) {
traces[i].visible = "legendonly";
}
}
$(this.divobj).html("");
Plotly.newPlot(this.divobj.id, traces, layout, { "modeBarButtonsToRemove": ['sendDataToCloud','hoverCompareCartesian'], "displaylogo": false});
}
});
};
},
},
init: function () {
moderator.api.init();
moderator.editor.init();
moderator.permissioneditor.init();
moderator.plots.init();
}
};
$( document ).ready( function () {
moderator.init();
} );
$( document ).ready( function () {
var l = $(".plot-view");
for (var i = 0; i < l.length; i ++) {
if (!l[i].id)
l[i].id = "plot-"+i;
$(l[i]).html('<div class="plot-loader"></div>');
$.ajax({
divobj: l[i],
method: "GET",
url: l[i].dataset.url,
dataType: "json",
error: function (jqXHR, textStatus, errorThrow) {
$(this.divobj).html('<div class="plot-error">'+errorThrow+'</div>');
},
success: function (traces) {
var layout = {margin: {l: 30, r: 30, t: 10, b: 70, pad: 0}};
for (var i = 0; i < traces.length; i ++) {
traces[i].type = this.divobj.dataset.type;
}
if (this.divobj.dataset.type == "pie")
layout.showlegend = false;
traces.sort(function (a, b) {
asum = 0;
bsum = 0;
for (var i = 0; i < a.y.length; i++)
asum += a.y[i]
for (var i = 0; i < b.y.length; i++)
bsum += b.y[i]
return bsum-asum;
});
for (var i = 0; i < traces.length; i++)
if (i > 20)
traces[i].visible = "legendonly";
$(this.divobj).html("");
Plotly.newPlot(this.divobj.id, traces, layout, { "modeBarButtonsToRemove": ['sendDataToCloud','hoverCompareCartesian'], "displaylogo": false});
}
});
};
});
$(window).on("resize", function () {
$(".plot-view").each(function () {Plotly.Plots.resize(this)});
});
......@@ -10,6 +10,7 @@
font-size: 3em;
text-align: center;
line-height: 130px;
text-shadow: 0 0 2px black;
}
#timetable.table-bordered td:first-child {
......
......@@ -2,12 +2,23 @@ from server import *
import json
from jobs import date_json_handler
from hashlib import md5
from datetime import datetime
@app.route('/internal/stats')
@app.route('/internal/stats/<semester>')
@register_navbar('Statistiken', icon='stats')
@mod_required
def stats():
return render_template('stats.html')
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)
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"
......@@ -18,6 +29,7 @@ statsqueries['organizer_courses'] = "SELECT courses.organizer AS labels, count(c
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"
def plotly_date_handler(obj):
return obj.strftime("%Y-%m-%d %H:%M:%S")
......@@ -45,10 +57,52 @@ def stats_viewsperday(req, param=""):
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 = ?'
queries = {
'lecture': 'SELECT log.date AS date, formats.description AS trace, COUNT(DISTINCT log.id) AS y FROM log 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 UNION 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',
'course': 'SELECT log.date AS date, formats.description 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 JOIN formats ON formats.id = videos.video_format WHERE log.date > %T AND lectures.course_id = ? GROUP BY log.date, videos.video_format UNION 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 WHERE log.date > %T AND lectures.course_id = ? GROUP BY log.date',
'global': 'SELECT log.date AS date, formats.description AS trace, COUNT(DISTINCT log.id) AS y 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 UNION SELECT log.date AS date, "total" AS trace, COUNT(DISTINCT log.id) AS y FROM log WHERE log.date > %T GROUP BY log.date',
'courses': 'SELECT log.date AS date, courses.handle 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 JOIN courses ON courses.id = lectures.course_id WHERE log.date > %T GROUP BY log.date, courses.id'
'lecture': # views per day per lecture (split per format)
'''SELECT log.date AS date, formats.description AS trace, COUNT(DISTINCT log.id) AS y
FROM log
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
UNION
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''',
'course': # views per day per format for a single course
'''SELECT log.date AS date, formats.description 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
JOIN formats ON formats.id = videos.video_format
WHERE log.date > %T AND lectures.course_id = ?
GROUP BY log.date, videos.video_format
UNION
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
WHERE log.date > %T AND lectures.course_id = ?
GROUP BY log.date''',
'global': # views per format per day (split per format)
'''SELECT log.date AS date, formats.description AS trace, COUNT(DISTINCT log.id) AS y
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
UNION
SELECT log.date AS date, "total" AS trace, COUNT(DISTINCT log.id) AS y
FROM log
WHERE log.date > %T
GROUP BY log.date''',
'courses': # views per course per day
'''SELECT log.date AS date, courses.handle 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
JOIN courses ON courses.id = lectures.course_id
WHERE log.date > %T
GROUP BY log.date, courses.id'''
}
expr = queries[req].replace('%T', '"'+query(date_subexpr%('viewsperday.'+req), param)[0]['t']+'"')
params = [param]*expr.count('?')
......@@ -71,6 +125,12 @@ def stats_viewsperday(req, param=""):
data[row['date']][row['trace']] = row['y']
end = date.today()
res = [{'name': trace, 'x': [], 'y': []} for trace in traces]
filter = request.args.get('filter')
if filter:
filter = filter.split('-')
start = date.fromtimestamp(int(filter[0]))
end = date.fromtimestamp(int(filter[1]))
while start and start <= end:
for trace in res:
trace['x'].append(start)
......
......@@ -42,8 +42,7 @@
<nav class="hidden-print navbar navbar-default navbar-static-top" {% if config.DEBUG %} style="background-color: red" {% endif %} >
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target=".navbar-collapse">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
......@@ -52,10 +51,26 @@
<a class="navbar-brand" href="/" style="padding: 3px;">
<img alt="Brand" src="{{url_for('static', filename='logo.png')}}" style="height: 44px; width: 44px" >
</a>
<ul class="nav nav-pills" style="margin-top: 5px; padding-left: 40px;">
{% for endpoint, caption, iconlib, gly, visible in navbar if visible %}
<li{% if endpoint == request.endpoint %} class="active"{% endif %}>
<a href="{{ url_for(endpoint) }}" style="padding: 10px 6px;">
{% if gly != '' %}
{% if iconlib == 'bootstrap' %}
<span aria-hidden="true" class="glyphicon glyphicon-{{ gly }}"></span>
{% elif iconlib == 'fa' %}
<span aria-hidden="true" class="fa fa-{{ gly }}"></span>
{% endif %}
{{ caption }}
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
<div class="collapse navbar-collapse">
<ul class="nav nav-pills" style="margin-top: 5px;">
{% for endpoint, caption, iconlib, gly, visible in navbar if visible or ismod() %}
{% for endpoint, caption, iconlib, gly, visible in navbar if (not visible) and ismod() %}
<li{% if endpoint == request.endpoint %} class="active"{% endif %}>
<a href="{{ url_for(endpoint) }}">
{% if gly != '' %}
......@@ -70,7 +85,7 @@
</li>
{% endfor %}
<li class="col-xs-12 col-sm-4 pull-right">
<li class="col-xs-9 col-sm-4 pull-right">
<form action="{{ url_for('search') }}" role="search">
<div class="input-group" style="margin-top: 3px">
<input class="form-control" type="text" name="q" placeholder="Search" value="{{ searchtext }}">
......@@ -187,7 +202,11 @@
{% endif %}
<script>
$( function () {
$('[data-toggle="tooltip"]').tooltip({ 'trigger': 'hover' });
$('[data-toggle="tooltip"]').tooltip(
{
trigger: 'hover',
html: true
});
});
</script>
</body>
......
......@@ -35,7 +35,7 @@
</tbody>
</table>
</div>
{% if ismod() %}
{% if ismod() %}
<div class="col-xs-12" style="margin-top: 20px">
<table class="table-condensed table-top-aligned">
<tbody>
......@@ -51,14 +51,45 @@
</tbody>
</table>
</div>
<div class="col-xs-6 plot-view" data-url="{{url_for('stats_viewsperday', req="course", param=course.id)}}"></div>
<div class="col-xs-6 plot-view" data-type="bar" data-url="{{url_for('stats_generic', req="lecture_views", param=course.id)}}"></div>
{% endif %}
{% endif %}
</div>
</div>
{% if ismod() %}
<div class="panel panel-default">
<div class="panel-heading">
<a data-toggle="collapse" href="#statspanel" class="plotlyresize"><h1 class="panel-title">Statistiken</h1></a>
</div>
<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>
<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>
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<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=request.url) }}">Neuer Termin</a><a class="btn btn-default" style="margin-right: 5px;" href="{{url_for('list_import_sources', id=course['id'])}}">Campus Import</a>{% endif %} <a class="fa fa-rss-square pull-right" aria-hidden="true" href="{{url_for('feed', handle=course.handle)}}" style="text-decoration: none"></a> </h1>
<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>
{% endif %}
<ul class="list-inline pull-right">
<li>
<a class="fa fa-rss-square" aria-hidden="true" href="{{url_for('feed', handle=course.handle)}}" style="text-decoration: none"></a>
</li>
{% if ismod() %}
<li>
<a class="fa fa-calendar" aria-hidden="true" href="{{url_for('ical_course', course=course.handle)}}" style="text-decoration: none"></a>
</li>
{% endif %}
</h1>
</div>
<ul class="list-group lectureslist">
{% for l in lectures %}
......@@ -66,4 +97,38 @@
{% endfor %}
</ul>
</div>
<script>
$.ajax({
method: "GET",
url: "{{url_for('stats_generic', req="lecture_views", param=course.id)}}",
dataType: "json",
error: function() {
var counter = $(".viewcounter");
for (var i=0; i<counter.length; i++) {
$(counter[i]).text("0");
}
},
success: function (traces) {
var dates={};
var t = traces[0];
if (!t.x) {
return;
}
for (var i=0; i<t.x.length; i++) {
dates[t.x[i]] = t.y[i];
}
var counter = $(".viewcounter");
for (var i=0; i<counter.length; i++) {
$(counter[i]).text(dates[$(counter[i]).data("lecturedate")]);
}
var counter = $(".viewcounter");
for (var i=0; i<counter.length; i++) {
if ($(counter[i]).text() == "loading...") {
$(counter[i]).text("0");
}
}
}
});
</script>
{% endblock %}
......@@ -8,6 +8,9 @@
<a class="fa fa-rss-square btn btn-default" aria-hidden="true" href="{{url_for('courses_feed')}}" style="text-decoration: none"></a>
</li>
{% if ismod() %}
<li>
<a class="fa fa-calendar btn btn-default" aria-hidden="true" href="{{url_for('ical_all')}}" style="text-decoration: none"></a>
</li>
<li>
{% set newhandle = 'new'+(randint(0,1000)|string) %}
<a class="btn btn-default" href="{{ url_for('create', table='courses', handle=newhandle, title='Neue Veranstaltung', responsible=session.user.givenName, ref=url_for('course', handle=newhandle)) }}">Neue Veranstaltung</a>
......@@ -56,7 +59,8 @@
<div class="panel-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion-{{ g.grouper|tagid }}" href="#{{g.grouper|tagid}}" style="color: #222;">
{% if groupedby == 'semester' %}
<h1 class="panel-title">{{g.grouper|semester(long=True)}} ({{g.list|length}} Veranstaltungen)</h1>
<h1 class="panel-title">{{g.grouper|semester(long=True)}} ({{g.list|length}} Veranstaltungen)
</h1>
{% else %}
<h1 class="panel-title">{{g.grouper}}</h1>
{% endif %}
......
......@@ -25,7 +25,7 @@
<div class="panel-body">
<div class="row" style="padding: 0px;">
<div class="col-xs-12" style="padding-bottom: 5px;">
<a href="{{url_for('course', handle=course.handle)}}#lecture-{{lecture.id}}" class="btn btn-default" >Zur Veranstaltungsseite</a>
<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>
......@@ -39,7 +39,6 @@
<div class="col-xs-12" style="padding-top: 20px">
<p>{{ moderator_editor(['lectures',lecture.id,'comment'], lecture.comment) }}</p>
</div>
{% if (chapters|length > 0) or ismod() %}
<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>
......@@ -71,14 +70,29 @@
{% endfor %}
</table>
</div>
{% endif %}
{% if ismod() %}
<div class="col-xs-12 plot-view" data-url="{{url_for('stats_viewsperday', req="lecture", param=lecture.id)}}"></div>
<div class="col-xs-12 plot-view" data-url="{{url_for('stats_generic', req="live_views", param=lecture.id)}}"></div>
{% endif %}