diff --git a/static/moderator.js b/static/moderator.js index ed9cb376f85ca7512b3d98e60bbdfcec5afc1bd6..6fa226f1dd7fcd2431ff0507bf834a0b985fc59f 100644 --- a/static/moderator.js +++ b/static/moderator.js @@ -250,56 +250,65 @@ var moderator = { } } }, + 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 > 30) { + 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)}); -}); diff --git a/stats.py b/stats.py index 8ef31660c07b1e13c41454b9ee5a716a23fbfa1c..d57e0fc8a09086d6ef7e00a3f05b2dda1f7f6b12 100644 --- a/stats.py +++ b/stats.py @@ -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) diff --git a/templates/course.html b/templates/course.html index 6bbb9e915a58af67c96861574ed0937f2ddd3f3a..d06c17ad4fa9db7568e3f7e2a494be230986bf91 100644 --- a/templates/course.html +++ b/templates/course.html @@ -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,11 +51,28 @@ </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> @@ -66,4 +83,23 @@ {% endfor %} </ul> </div> + +<script> +$.ajax({ + method: "GET", + url: "{{url_for('stats_generic', req="lecture_views", param=course.id)}}", + dataType: "json", + success: function (traces) { + var dates={}; + var t = traces[0] + 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")]); + } + } +}); +</script> {% endblock %} diff --git a/templates/lecture.html b/templates/lecture.html index 82e27aed456468a958ea315c1c42ad96688f4104..a14515c80eff18a9049e04dd4a575e4f35c80489 100644 --- a/templates/lecture.html +++ b/templates/lecture.html @@ -70,13 +70,30 @@ {% endfor %} </table> </div> - {% 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 %} </div> </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="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)}}"></div> + </div> + </div> +</div> +{% endif %} + + <script> function hintchapterclick (src) { $.ajax({ @@ -132,4 +149,4 @@ $(document).ready(function() { }); }); </script> -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/macros.html b/templates/macros.html index cc40485bcd93ec93b64cac819e04037b71cc04fa..92cc436e7ea89cf9b0c009aadacb4245bc1d9e26 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -226,16 +226,25 @@ $('#embedcodebtn').popover( <li>Hörsaal: {{ moderator_editor(['lectures',lecture.id,'place'], lecture.place) }} </li> {% endif %} </ul> - <ul class="list-inline col-sm-4 col-xs-12"> - <li class="dropdown"> - {{ video_download_btn(videos) }} - </li> - <li class="pull-right"> - {{ moderator_permissioneditor('lecture', lecture.id, lecture.perm, global_permissions) }} + <ul class="col-sm-4 col-xs-12 list-unstyled"> + <li> + <ul class="list-inline"> + <li class="dropdown"> + {{ video_download_btn(videos) }} + </li> + <li class="pull-right"> + {{ moderator_permissioneditor('lecture', lecture.id, lecture.perm, global_permissions) }} + </li> + <li class="pull-right"> + {{ moderator_delete(['lectures',lecture.id,'deleted']) }} + </li> + </ul> </li> + {% if ismod() %} <li class="pull-right"> - {{ moderator_delete(['lectures',lecture.id,'deleted']) }} + <p>Zuschauer: <span data-lectureid="{{ lecture.id }}" data-lecturedate="{{ lecture.time }}" class="viewcounter">loading...</span></p> </li> + {% endif %} </ul> {% else %} <div class="col-sm-2 col-xs-12"> diff --git a/templates/stats.html b/templates/stats.html index c7cb96aab7b11b643767610fe6534465afd8ae93..099673eb4f99c08dc882ee1e719c9ccb833015d2 100644 --- a/templates/stats.html +++ b/templates/stats.html @@ -3,7 +3,7 @@ <div class="panel-group"> <div class="panel panel-default"> <div class="panel-heading"> - <h1 class="panel-title">Statistiken</h1> + <h1 class="panel-title">Gesamt</h1> </div> <div class="panel-body"> <div class="row col-xs-12"> @@ -11,13 +11,31 @@ <div class="col-xs-12 col-md-6 plot-view" data-url="{{url_for('stats_generic', req="lectures_count")}}"></div> <div class="col-xs-12 col-md-6 plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="categories_courses")}}"></div> <div class="col-xs-12 col-md-6 plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="categories_lectures")}}"></div> - <!--<div class="col-xs-12 col-md-6 plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="organizer_courses")}}"></div>--> - <!--<div class="col-xs-12 col-md-6 plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="formats_views")}}"></div>--> - <div class="col-xs-12 plot-view" data-url="{{url_for('stats_viewsperday', req="global")}}"></div> - <div class="col-xs-12 plot-view" data-url="{{url_for('stats_viewsperday', req="courses")}}"></div> + <div class="col-xs-12 col-md-6 plot-view" data-type="pie" data-url="{{url_for('stats_generic', req="organizer_courses")}}"></div> + </div> + </div> + </div> + <div class="panel panel-default"> + <div class="panel-heading"> + <span class="panel-title"><a name="semesterstats"></a>Semester <select id="semesterselect" name="semester"><option value="">alle</option></select></span> + </div> + <div class="panel-body" > + <div class="row col-xs-12"> + <div class="col-xs-12 plot-view" data-url="{{url_for('stats_viewsperday', req="global", filter=filter)}}"></div> + <div class="col-xs-12 plot-view" data-url="{{url_for('stats_viewsperday', req="courses", filter=filter)}}"></div> </div> </div> </div> </div> +<script> +$( document ).ready(function () { + {% for s in semester if s.semester != '' %} + $("#semesterselect").append('<option value="{{ s.from.timestamp()|int }}-{{ s.to.timestamp()|int }}">{{ s.semester }}</option>'); + {% endfor %} + $("#semesterselect").val("{{ filter }}") + $("#semesterselect").on("change", function () { + window.location.href="{{ url_for('stats') }}?filter="+$("#semesterselect").val()+"#semesterstats"; + }); +}); </script> {% endblock %}