From 792f070d0f3e9d09e37272785f20f9bff92ad2b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ji=C5=99=C3=AD=20Setni=C4=8Dka?= <setnicka@seznam.cz>
Date: Mon, 22 Mar 2021 23:41:09 +0100
Subject: [PATCH] =?UTF-8?q?Zpr=C3=A1vi=C4=8Dky:=20Zobrazen=C3=AD=20a=20pra?=
 =?UTF-8?q?videln=C3=BD=20reload=20v=20=C3=BA=C4=8Dastnick=C3=A9=20=C4=8D?=
 =?UTF-8?q?=C3=A1sti?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reloaduje se jednou za 30s z JSON endpointu, při změně počtu zpráv začne blikat
záhlaví okna a nová zprávička se zvýrazní.
---
 mo/web/__init__.py                      |  1 +
 mo/web/templates/parts/user_news.html   |  6 ++
 mo/web/templates/user_contest.html      | 14 +++++
 mo/web/templates/user_contest_task.html | 13 ++++
 mo/web/user.py                          | 29 ++++++++-
 static/js/news-reloader.js              | 81 +++++++++++++++++++++++++
 6 files changed, 141 insertions(+), 3 deletions(-)
 create mode 100644 mo/web/templates/parts/user_news.html
 create mode 100644 static/js/news-reloader.js

diff --git a/mo/web/__init__.py b/mo/web/__init__.py
index 57d40439..61f8fa51 100644
--- a/mo/web/__init__.py
+++ b/mo/web/__init__.py
@@ -114,6 +114,7 @@ mo.ext.assets.Assets(app, url_prefix='/assets', asset_dir=static_dir)
 app.assets.add_assets([
     'bootstrap.min.css',
     'mo.css',
+    'js/news-reloader.js',
 ])
 
 
diff --git a/mo/web/templates/parts/user_news.html b/mo/web/templates/parts/user_news.html
new file mode 100644
index 00000000..b873d205
--- /dev/null
+++ b/mo/web/templates/parts/user_news.html
@@ -0,0 +1,6 @@
+<div id="novinky">
+{% include "parts/messages.html" %}
+</div>
+<script type="text/javascript">
+r = new NewsReloader(document.getElementById("novinky"), "{{ url }}", 60000);
+</script>
diff --git a/mo/web/templates/user_contest.html b/mo/web/templates/user_contest.html
index d784945c..c676c073 100644
--- a/mo/web/templates/user_contest.html
+++ b/mo/web/templates/user_contest.html
@@ -3,6 +3,12 @@
 {% set round = contest.round %}
 {% set state = contest.ct_state() %}
 
+{% block head %}
+{% if contest.round.has_messages %}
+	<script src="{{ asset_url('js/news-reloader.js') }}" type="text/javascript"></script>
+{% endif %}
+{% endblock %}
+
 {% block title %}{{ round.name }} {{ round.year }}. ročníku kategorie {{ round.category }}: {{ contest.place.name }}{% endblock %}
 {% block breadcrumbs %}
 <li><a href='{{ url_for('user_index') }}'>Soutěže</a>
@@ -98,4 +104,12 @@
 </table>
 
 {% endif %}
+
+{% if contest.round.has_messages %}
+<h3>Novinky k soutěži</h3>
+{% with title="Novinky k soutěži", url=url_for('user_contest_news', id=contest.contest_id) %}
+	{% include "parts/user_news.html" %}
+{% endwith %}
+{% endif %}
+
 {% endblock %}
diff --git a/mo/web/templates/user_contest_task.html b/mo/web/templates/user_contest_task.html
index 525f1071..01c3263b 100644
--- a/mo/web/templates/user_contest_task.html
+++ b/mo/web/templates/user_contest_task.html
@@ -3,6 +3,12 @@
 {% set round = contest.round %}
 {% set state = contest.ct_state() %}
 
+{% block head %}
+{% if contest.round.has_messages %}
+	<script src="{{ asset_url('js/news-reloader.js') }}" type="text/javascript"></script>
+{% endif %}
+{% endblock %}
+
 {% block title %}Úloha {{ task.code }}: {{ task.name }}{% endblock %}
 {% block breadcrumbs %}
 <li><a href='{{ url_for('user_index') }}'>Soutěže</a>
@@ -85,4 +91,11 @@
 {% endif %}
 {% endif %}
 
+{% if contest.round.has_messages %}
+<h3>Novinky k soutěži</h3>
+{% with title="Novinky k soutěži", url=url_for('user_contest_news', id=contest.contest_id) %}
+	{% include "parts/user_news.html" %}
+{% endwith %}
+{% endif %}
+
 {% endblock %}
diff --git a/mo/web/user.py b/mo/web/user.py
index a4cf524e..e54667f5 100644
--- a/mo/web/user.py
+++ b/mo/web/user.py
@@ -1,17 +1,17 @@
-from flask import render_template, request, g, redirect, url_for, flash
+from flask import render_template, jsonify, g, redirect, url_for, flash
 from flask_wtf import FlaskForm
 import flask_wtf.file
 from sqlalchemy import and_
 from sqlalchemy.orm import joinedload
 import werkzeug.exceptions
 import wtforms
-import wtforms.validators as validators
 
 import mo.config as config
 import mo.db as db
 import mo.submit
 import mo.util
 from mo.util import logger
+from mo.util_format import time_and_timedelta
 from mo.web import app
 import mo.web.util
 
@@ -66,9 +66,12 @@ def get_task(contest: db.Contest, id: int) -> db.Task:
 
 @app.route('/user/contest/<int:id>/')
 def user_contest(id: int):
+    sess = db.get_session()
     contest = get_contest(id)
 
-    task_sols = (db.get_session().query(db.Task, db.Solution)
+    messages = sess.query(db.Message).filter_by(round_id=contest.round_id).order_by(db.Message.created_at).all()
+
+    task_sols = (sess.query(db.Task, db.Solution)
                  .select_from(db.Task)
                  .outerjoin(db.Solution, and_(db.Solution.task_id == db.Task.task_id, db.Solution.user == g.user))
                  .filter(db.Task.round == contest.round)
@@ -81,10 +84,27 @@ def user_contest(id: int):
         'user_contest.html',
         contest=contest,
         task_sols=task_sols,
+        messages=messages,
         max_submit_size=config.MAX_CONTENT_LENGTH,
     )
 
 
+@app.route('/user/contest/<int:id>/news')
+def user_contest_news(id: int):
+    sess = db.get_session()
+    contest = get_contest(id)
+
+    messages = sess.query(db.Message).filter_by(round_id=contest.round_id).order_by(db.Message.created_at).all()
+
+    out_messages = [{
+        'title': msg.title,
+        'date_format': time_and_timedelta(msg.created_at),
+        'body': msg.html,
+    } for msg in messages]
+
+    return jsonify(out_messages)
+
+
 @app.route('/user/contest/<int:id>/task-statement/zadani.pdf')
 def user_task_statement(id: int):
     contest = get_contest(id)
@@ -108,6 +128,8 @@ def user_contest_task(contest_id: int, task_id: int):
     task = get_task(contest, task_id)
     sess = db.get_session()
 
+    messages = sess.query(db.Message).filter_by(round_id=contest.round_id).order_by(db.Message.created_at).all()
+
     state = contest.ct_state()
     if state == db.RoundState.preparing:
         # Dokud se kolo připravuje nebo čeká na zveřejnění zadání, tak ani nezobrazujeme
@@ -164,6 +186,7 @@ def user_contest_task(contest_id: int, task_id: int):
         sol=sol,
         papers=papers,
         form=form,
+        messages=messages,
     )
 
 
diff --git a/static/js/news-reloader.js b/static/js/news-reloader.js
new file mode 100644
index 00000000..04fe721d
--- /dev/null
+++ b/static/js/news-reloader.js
@@ -0,0 +1,81 @@
+class NewsReloader {
+	news_count = 0;
+	notification_interval = null;
+	original_title = "";
+
+	constructor(element, url, check_interval=60000) {
+		this.element = element;
+		this.url = url;
+		this.check_interval = check_interval;
+
+		this.news_count = element.childElementCount;
+		this.original_title = document.title;
+
+		var t = this
+		setInterval(function() { t.refreshNews();}, this.check_interval);
+		window.addEventListener('focus', function() {
+			t.news_count = t.element.childElementCount;
+			t.notificationOff();
+		});
+	}
+
+	notificationOn(notification) {
+		clearInterval(this.notification_interval); // clear any previous interval
+		var t = this;
+		this.notification_interval = setInterval(function() {
+			document.title = (t.original_title == document.title)
+					? notification + t.original_title
+					: t.original_title;
+		}, 1000);
+	}
+
+	notificationOff() {
+		if (this.notification_interval) {
+			clearInterval(this.notification_interval);
+			document.title = this.original_title;
+		}
+	}
+
+	refreshNews() {
+		var xmlhttp = new XMLHttpRequest();
+		var t = this;
+		xmlhttp.onreadystatechange = function() {
+			if (this.readyState == 4 && this.status == 200) {
+				var newsArr = JSON.parse(this.responseText);
+				var count = newsArr.length
+
+				var markN = 0; // how many elements to mark with class "new"
+				if (count > t.news_count) {
+					markN = count - t.news_count;
+				}
+
+				// Create all new <div>s
+				var newElements = document.createDocumentFragment();
+				for (var i = 0; i < count; i++) {
+					var div = document.createElement("div");
+					div.className = "message";
+					if (i + markN >= count) div.className += " new"; // mark N last elements
+					div.innerHTML = "<span class='msg-title'>" + newsArr[i]["title"] + "</span>"
+							+"<span class='msg-date'>" + newsArr[i]["date_format"] + "</span>"
+							+"<div class='msg-text'>" + newsArr[i]["body"] + "</div>";
+					newElements.appendChild(div);
+				}
+
+				// Remove all childs and append new
+				t.element.innerHTML = "";
+				t.element.appendChild(newElements);
+
+				// Notification
+				if (count != t.news_count) {
+					if (document.hasFocus()) t.news_count = count;
+					else {
+						// Add (1) to the title (with removing any previous value)
+						t.notificationOn("(" + Math.abs(count - t.news_count).toString() + ") ");
+					}
+				}
+			}
+		}
+		xmlhttp.open("GET", this.url, true);
+		xmlhttp.send();
+	}
+}
-- 
GitLab