From 72ffb8b3a4ecd497a08fc677a574c991d02c42cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Kon=C4=8Dick=C3=BD?=
 <koncicky@kam.mff.cuni.cz>
Date: Fri, 26 Feb 2021 17:30:19 +0100
Subject: [PATCH 1/7] Student grading: Database support

---
 db.ddl | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/db.ddl b/db.ddl
index 54ce5cf..8538518 100644
--- a/db.ddl
+++ b/db.ddl
@@ -22,7 +22,8 @@ CREATE TABLE owl_courses (
 	cid		serial		PRIMARY KEY,
 	ident		varchar(255)	UNIQUE NOT NULL,
 	name		varchar(255)	NOT NULL,
-	enroll_token	varchar(255)	UNIQUE NOT NULL
+	enroll_token	varchar(255)	UNIQUE NOT NULL,
+	student_grading	boolean		NOT NULL DEFAULT FALSE
 );
 
 CREATE TABLE owl_enroll (
@@ -50,6 +51,12 @@ CREATE TABLE owl_topics (
 	UNIQUE(cid, ident)
 );
 
+CREATE TABLE owl_student_graders (
+	tid		int		NOT NULL REFERENCES owl_topics(tid) ON DELETE CASCADE,
+	uid		int		NOT NULL REFERENCES owl_users(uid) ON DELETE CASCADE,
+	UNIQUE(tid, uid)
+);
+
 CREATE TABLE owl_posts (
 	pid		serial		PRIMARY KEY,
 	tid		int		NOT NULL REFERENCES owl_topics(tid) ON DELETE CASCADE,
-- 
GitLab


From 1f711edd5f5dad03c565a8ffad7f17ded759bc2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Kon=C4=8Dick=C3=BD?=
 <koncicky@kam.mff.cuni.cz>
Date: Fri, 26 Feb 2021 17:31:48 +0100
Subject: [PATCH 2/7] Student grading support in topics administration

---
 owl.py                       | 5 +++++
 templates/admin-course.html  | 1 +
 templates/admin-courses.html | 2 ++
 templates/admin-topics.html  | 4 ++++
 4 files changed, 12 insertions(+)

diff --git a/owl.py b/owl.py
index afee873..ea66da4 100644
--- a/owl.py
+++ b/owl.py
@@ -811,6 +811,11 @@ def admin_topics(cident):
     return render_template('admin-topics.html', topics=topics)
 
 
+@app.route('/admin/topics/<cident>/graders/<tid>', methods=('GET', 'POST'))
+def admin_topic_graders(cident, tid):
+    return "TODO"
+
+
 class EditTopicForm(FlaskForm):
     rank = wtforms_html5.IntegerField("Rank:", validators=[validators.required()])
     ident = wtforms.StringField("Name:")
diff --git a/templates/admin-course.html b/templates/admin-course.html
index af06d53..3f43893 100644
--- a/templates/admin-course.html
+++ b/templates/admin-course.html
@@ -7,6 +7,7 @@
 		<tr><th>Name<td>{{ g.course.name }}
 		<tr><th>Enroll token<td><code>{{ g.course.enroll_token }}</code>
 		<tr><th>Teachers<td>{{ teachers|sort|join(', ') }}
+		<tr><th>Student grading<td>{{ 'enabled' if g.course.student_grading else 'disabled' }}
 	</table>
 
 	<div class=buttons>
diff --git a/templates/admin-courses.html b/templates/admin-courses.html
index 0f42fc9..682e25e 100644
--- a/templates/admin-courses.html
+++ b/templates/admin-courses.html
@@ -7,6 +7,7 @@
 			<th>Ident
 			<th>Name
 			<th>Teachers
+			<th>Student grading
 		</thead>
 	{% for c in courses %}
 		<tr>
@@ -17,6 +18,7 @@
 		{% else %}
 			<td>–
 		{% endif %}
+		<td>{{ 'enabled' if c.student_grading else 'disabled' }}
 	{% endfor %}
 	</table>
 
diff --git a/templates/admin-topics.html b/templates/admin-topics.html
index 700891f..01ff0ff 100644
--- a/templates/admin-topics.html
+++ b/templates/admin-topics.html
@@ -5,6 +5,7 @@
 
 	<table class=topics>
 	<tr><th>Rank<th>Name<th>Title<th>Type<th>Deadline<th>Points
+	{% if g.course.student_grading %}<th>Student graders{% endif %}
 	{% for t in topics %}
 		{% if t.public %}
 			{% set cls = "told" %}
@@ -22,6 +23,9 @@
 			<td>{{ {'H': 'heading', 'T': 'task', 'D': 'discuss' }.get(t.type, t.type) }}
 			<td>{{ t.deadline|reltimeformat }}
 			<td class=pts>{{ t.max_points if t.max_points != None else "" }}
+			{% if g.course.student_grading and t.type == 'T' %}
+			<td><a href='{{ url_for('admin_topic_graders', cident=g.course.ident, tid=t.tid) }}'>Assign</a>
+			{% endif %}
 	{% endfor %}
 	</table>
 
-- 
GitLab


From 403235bfbf95260092253e032a008f8fa4367913 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Kon=C4=8Dick=C3=BD?=
 <koncicky@kam.mff.cuni.cz>
Date: Fri, 26 Feb 2021 18:11:43 +0100
Subject: [PATCH 3/7] Student grader now acts as a new grade role and sees
 grade action

---
 owl.py                   | 57 +++++++++++++++++++++++++++++++++-------
 templates/course.html    |  6 +++++
 templates/edit-post.html |  2 +-
 templates/topic.html     |  4 +--
 4 files changed, 56 insertions(+), 13 deletions(-)

diff --git a/owl.py b/owl.py
index ea66da4..8a9450e 100644
--- a/owl.py
+++ b/owl.py
@@ -236,6 +236,12 @@ def topic_init(cident, tident):
     if not g.topic.public and not g.is_teacher and not g.is_admin:
         return "This is a private topic", HTTPStatus.FORBIDDEN
 
+    if g.course.student_grading:
+        db_query("SELECT * FROM owl_student_graders WHERE tid=%s AND uid=%s", (g.topic.tid, g.uid));
+        g.is_grader = g.is_teacher or bool(db.fetchone())
+    else:
+        g.is_grader = g.is_teacher
+
     return None
 
 
@@ -264,7 +270,14 @@ def course_index(cident):
     else:
         target_uid = g.uid
 
-    db_query("""
+    if g.course.student_grading:
+        sql_is_grader = "EXISTS (SELECT * FROM owl_student_graders x WHERE x.tid=t.tid AND x.uid=%s)"
+        sql_is_grader_params = [g.uid]
+    else:
+        sql_is_grader = "FALSE"
+        sql_is_grader_params = []
+
+    db_query(f"""
             SELECT t.cid, t.tid, t.ident, t.title, t.type, t.deadline, t.max_points,
                 s.seen AS last_seen,
                 (SELECT MAX(x.created)
@@ -279,7 +292,8 @@ def course_index(cident):
                    AND x.points IS NOT NULL
                  ORDER BY x.created DESC
                  LIMIT 1
-                ) AS points
+                ) AS points,
+                {sql_is_grader} AS is_grader
             FROM owl_topics t
             LEFT JOIN owl_seen s ON s.tid = t.tid
                                  AND s.observer_uid = %s
@@ -287,22 +301,26 @@ def course_index(cident):
             WHERE t.cid = %s
               AND t.public = true
             ORDER by rank, created
-        """, (g.uid, g.uid, g.uid, target_uid, g.course.cid))
+        """, [g.uid, g.uid] + sql_is_grader_params + [g.uid, target_uid, g.course.cid])
     topics = db.fetchall()
 
     course_total = Decimal('0.00')
     course_max = Decimal('0.00')
+    is_grader = False
     for t in topics:
         if t.points is not None:
             course_total += t.points
         if t.max_points is not None:
             course_max += t.max_points
+        if t.is_grader:
+            is_grader = True
 
     return render_template(
         'course.html',
         topics=topics,
         course_total=course_total,
-        course_max=course_max)
+        course_max=course_max,
+        is_grader=is_grader)
 
 
 ### Posts ###
@@ -359,8 +377,8 @@ def topic_index(cident, tident, student_uid=None):
         return err
 
     if student_uid is not None:
-        if not g.is_teacher:
-            return "Only teachers are allowed to do that", HTTPStatus.FORBIDDEN
+        if not g.is_grader:
+            return "Only graders are allowed to do that", HTTPStatus.FORBIDDEN
     else:
         if g.topic.type == 'D':
             # In discussion threads, all posts are global
@@ -426,7 +444,7 @@ def topic_post(form, student_uid):
         app.logger.info("Uploaded file: id=%s by_uid=%d", id, g.uid)
 
     points = None
-    if g.is_teacher and form.points.data is not None:
+    if g.is_grader and form.points.data is not None:
         points = form.points.data
 
     if comment or attach or points is not None:
@@ -472,6 +490,8 @@ def topic_post(form, student_uid):
             ) + "#p" + str(new_post.pid))
     elif g.is_teacher:
         return redirect(url_for('teacher'))
+    elif g.is_grader:
+        return redirect(url_for('topic_student_grade', cident=g.course.ident, tident=g.topic.ident))
     else:
         return redirect(url_for('course_index', cident=g.course.ident))
 
@@ -493,6 +513,15 @@ def edit_allowed_p(post):
             post.author_uid == g.uid and g.topic.public)
 
 
+@app.route('/c/<cident>/<tident>/grade')
+def topic_student_grade(cident=None, tident=None):
+    err = topic_init(cident, tident)
+    if err:
+        return err
+
+    return "TODO"
+
+
 @app.route('/post/<cident>/<int:pid>/', methods=('GET', 'POST'))
 def edit_post(cident, pid):
     err = course_init(cident)
@@ -508,6 +537,12 @@ def edit_post(cident, pid):
     g.topic = db.fetchone()
     assert g.topic
 
+    if g.course.student_grading:
+        db_query("SELECT * FROM owl_student_graders WHERE tid=%s AND uid=%s", (g.topic.tid, g.uid));
+        g.is_grader = g.is_teacher or bool(db.fetchone())
+    else:
+        g.is_grader = g.is_teacher
+
     if not edit_allowed_p(post):
         return "Not allowed to edit this post", HTTPStatus.FORBIDDEN
 
@@ -516,7 +551,7 @@ def edit_post(cident, pid):
         if not form.validate_on_submit():
             return render_template('edit-post.html', form=form)
 
-        if g.is_teacher and post.target_uid >= 0:
+        if g.is_grader and post.target_uid >= 0:
             student_uid = post.target_uid
         else:
             student_uid = None
@@ -529,15 +564,17 @@ def edit_post(cident, pid):
                 db_connection.commit()
                 return redirect(return_to)
             cmt = "*Post deleted by its author.*"
+            points = None
         else:
             cmt = strip_comment(form.comment.data)
+            points = form.points.data
 
         changed = False
         if cmt != post.comment:
             db_query("UPDATE owl_posts SET comment=%s WHERE pid=%s", (cmt, pid))
             changed = True
-        if g.is_teacher and form.points.data != post.points:
-            db_query("UPDATE owl_posts SET points=%s WHERE pid=%s", (form.points.data, pid))
+        if g.is_grader and points != post.points:
+            db_query("UPDATE owl_posts SET points=%s WHERE pid=%s", (points, pid))
             changed = True
 
         if changed:
diff --git a/templates/course.html b/templates/course.html
index 661cc22..7e27899 100644
--- a/templates/course.html
+++ b/templates/course.html
@@ -23,6 +23,7 @@
 					{% set ns.have_table = True %}
 					<table class=topics>
 					<tr><th>Topic<th>Deadline<th>Points<th>Max
+					{% if is_grader %}<th>Action{% endif %}
 				{% endif %}
 				{% if t.last_posted != None and (t.last_seen == None or t.last_posted > t.last_seen) %}
 					{% set cls = 'tnew' %}
@@ -33,6 +34,11 @@
 				<td>{{ t.deadline|reltimeformat }}
 				<td class=pts>{{ t.points if t.points != None else "" }}
 				<td class=pts>{{ t.max_points if t.max_points != None else "" }}
+				{% if is_grader %}
+					<td>{% if t.is_grader %}
+						<a href="{{ url_for('topic_student_grade', cident=g.course.ident, tident=t.ident) }}">Grade</a>
+					{% endif %}
+				{% endif %}
 			{% else %}
 				{% if ns.have_table == True %}
 					{% set ns.have_table = False %}
diff --git a/templates/edit-post.html b/templates/edit-post.html
index d9bdcde..0d83781 100644
--- a/templates/edit-post.html
+++ b/templates/edit-post.html
@@ -9,7 +9,7 @@
 			<p class=error>{{ form.errors['comment'][0] }}
 		{% endif %}
 		<p>{{ form.comment(rows=16, cols=80) }}
-		{% if g.is_teacher %}
+		{% if g.is_grader %}
 			<p>{{ form.points.label }} {{ form.points(size=6) }} / {{ g.topic.max_points or "?" }}
 		{% endif %}
 		<div id=previewbox>
diff --git a/templates/topic.html b/templates/topic.html
index 3bd47e1..1d13109 100644
--- a/templates/topic.html
+++ b/templates/topic.html
@@ -43,7 +43,7 @@
 			{% set when=p.created %}
 		{% endif %}
 		{{ when|reltimeformat }}
-		{% if g.is_teacher and p.author_uid == student_uid and g.topic.deadline != None and when > g.topic.deadline %}
+		{% if g.is_grader and p.author_uid == student_uid and g.topic.deadline != None and when > g.topic.deadline %}
 			<span class=phpast>(after deadline)</span>
 		{% endif %}
 		{% if g.is_teacher or p.author_uid == g.uid %}
@@ -85,7 +85,7 @@
 			<p class=error>{{ form.errors['attachment'][0] }}
 		{% endif %}
 		<p>{{ form.attachment.label }} {{ form.attachment() }}
-		{% if g.is_teacher %}
+		{% if g.is_grader %}
 			<p>{{ form.points.label }} {{ form.points(size=6) }} / <button id=maxbutton type=button onclick="javascript:document.getElementById('points').value = document.getElementById('maxbutton').innerText">{{ g.topic.max_points or "?" }}</button>
 		{% endif %}
 		<div id=previewbox>
-- 
GitLab


From 2913991cba20e9f0ba63e4fb1f4740b51b13ba08 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Kon=C4=8Dick=C3=BD?=
 <koncicky@kam.mff.cuni.cz>
Date: Fri, 5 Mar 2021 19:57:58 +0100
Subject: [PATCH 4/7] Page to grade a topic

---
 owl.py                     | 54 +++++++++++++++++++++++++++++++++++++-
 templates/topic-grade.html | 39 +++++++++++++++++++++++++++
 templates/topic.html       |  2 ++
 3 files changed, 94 insertions(+), 1 deletion(-)
 create mode 100644 templates/topic-grade.html

diff --git a/owl.py b/owl.py
index 8a9450e..8d8c577 100644
--- a/owl.py
+++ b/owl.py
@@ -519,7 +519,59 @@ def topic_student_grade(cident=None, tident=None):
     if err:
         return err
 
-    return "TODO"
+    if not g.is_grader:
+        return "Only graders can grade", HTTPStatus.FORBIDDEN
+
+    db_query("""
+            SELECT s.uid, s.full_name
+            FROM owl_enroll e
+            JOIN owl_users s ON s.uid = e.uid
+            WHERE e.cid = %s
+              AND e.is_teacher = false
+            ORDER BY s.full_name
+        """, (g.course.cid,))
+
+    students = {}
+    for s in db.fetchall():
+        students[s.uid] = s
+
+    solutions = {}
+    for uid, s in students.items():
+        solutions[uid] = TSolution(
+            points=None,
+            last_activity=None,
+            last_seen_by_me=None,
+            last_seen_by_teacher=None,
+        )
+
+    db_query("""
+            SELECT p.target_uid AS target_uid, p.created AS post_created, p.points
+            FROM owl_posts p
+            JOIN owl_users s ON s.uid = p.target_uid
+            WHERE p.tid = %s
+              AND p.target_uid >= 0
+            ORDER BY p.target_uid, p.created, p.pid
+        """, (g.topic.tid,))
+
+    for p in db.fetchall():
+        s = solutions[p.target_uid]
+        if p.points is not None:
+            s.points = p.points
+        s.last_activity = p.post_created
+
+    db_query("""
+            SELECT s.target_uid, s.seen
+            FROM owl_seen s
+            WHERE s.tid = %s
+              AND s.observer_uid = %s
+        """, (g.topic.tid, g.uid))
+
+    for t in db.fetchall():
+        if t.target_uid in solutions:
+            s = solutions[t.target_uid]
+            s.last_seen_by_me = t.seen
+
+    return render_template('topic-grade.html', students=students, solutions=solutions)
 
 
 @app.route('/post/<cident>/<int:pid>/', methods=('GET', 'POST'))
diff --git a/templates/topic-grade.html b/templates/topic-grade.html
new file mode 100644
index 0000000..6f66481
--- /dev/null
+++ b/templates/topic-grade.html
@@ -0,0 +1,39 @@
+{% extends "base.html" %}
+{% block body %}
+	{% set c=g.course %}
+	{% set t=g.topic %}
+	<h2>{{c.name}}</h2>
+
+		<p><a href='{{ url_for('course_index', cident=c.ident) }}'>Return to course</a>
+
+	<h3>Grade {{t.title}}</h3>
+		<table class=results>
+			<tr><th class='tbeforehdr'>Full name
+			<th>Points
+			<tr><th class='tbeforehdr'>max.
+			<td class='pts'>{{ t.max_points if t.max_points != None else "" }}
+		{% for s in students.values() %}
+			<tr><td class='tbeforehdr'>{{ s.full_name }}
+				{% set sol=solutions[s.uid] %}
+				{% set cls=[] %}
+				{% if sol.last_activity == None %}
+					{% do cls.append("snull") %}
+					<td class='{{ cls|join(" ") }}'>
+				{% else %}
+					{% do cls.append("pts") %}
+					{% if sol.last_seen_by_me == None or sol.last_seen_by_me < sol.last_activity %}
+						{% do cls.append("snew") %}
+					{% else %}
+						{% do cls.append("sold") %}
+					{% endif %}
+					<td class="{{ cls|join(" ") }}"><a href='{{ url_for('topic_index', cident=c.ident, tident=t.ident, student_uid=s.uid) }}'>
+						{% if sol.points != None %}
+							{{ sol.points }}
+						{% else %}
+							???
+						{% endif %}
+					</a>
+				{% endif %}
+		{% endfor %}
+		</table>
+{% endblock %}
diff --git a/templates/topic.html b/templates/topic.html
index 1d13109..711b5fd 100644
--- a/templates/topic.html
+++ b/templates/topic.html
@@ -10,6 +10,8 @@
 	<p><a href='{{ url_for('course_index', cident=g.course.ident) }}'>Back to the course&hellip;</a>&nbsp;&nbsp;
 	{% if g.is_teacher %}
 		&nbsp;&nbsp;<a href='{{ url_for('teacher') }}'>Teacher's summary</a>
+	{% elif g.is_grader %}
+		&nbsp;&nbsp;<a href='{{ url_for('topic_student_grade', cident=g.course.ident, tident=g.topic.ident) }}'>Grade&hellip;</a>
 	{% endif %}
 
 	<h3>{{ g.topic.title }}{% if not g.topic.public %} [private]{% endif %}</h3>
-- 
GitLab


From 8740999239c94b3a73911f25ea4f71000d5098ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Kon=C4=8Dick=C3=BD?=
 <koncicky@kam.mff.cuni.cz>
Date: Fri, 5 Mar 2021 19:58:46 +0100
Subject: [PATCH 5/7] Student grading assignment page for topic

---
 owl.py                             | 63 +++++++++++++++++++++++++++++-
 templates/admin-topic-graders.html | 32 +++++++++++++++
 2 files changed, 94 insertions(+), 1 deletion(-)
 create mode 100644 templates/admin-topic-graders.html

diff --git a/owl.py b/owl.py
index 8d8c577..5792072 100644
--- a/owl.py
+++ b/owl.py
@@ -899,10 +899,71 @@ def admin_topics(cident):
 
     return render_template('admin-topics.html', topics=topics)
 
+class AssignGraderForm(FlaskForm):
+    uid = wtforms.HiddenField()
+    assign = wtforms.SubmitField()
+    unassign = wtforms.SubmitField()
+
+    def validate(self):
+        if self.assign.data and self.unassign.data:
+            return False
+        return True
+
 
 @app.route('/admin/topics/<cident>/graders/<tid>', methods=('GET', 'POST'))
 def admin_topic_graders(cident, tid):
-    return "TODO"
+    err = course_init(cident) or must_be_teacher()
+    if err:
+        return err
+
+    if not g.course.student_grading:
+        return "Course does not allow student grading", HTTPStatus.FORBIDDEN
+
+    if tid is None:
+        return "Topic needed", HTTPStatus.NOT_FOUND
+
+    db_query('SELECT * FROM owl_topics WHERE cid=%s AND tid=%s', (g.course.cid, tid))
+    topic = db.fetchone()
+    if not topic:
+        return "No such topic", HTTPStatus.NOT_FOUND
+
+    if request.method == 'POST':
+        form = AssignGraderForm()
+
+        db_query('SELECT * FROM owl_enroll WHERE uid=%s AND cid=%s AND is_teacher=FALSE', (form.uid.data, g.course.cid))
+        if not db.fetchone():
+                return "Given uid is not a student of this course", HTTPStatus.BAD_REQUEST
+
+        db_query('SELECT * FROM owl_student_graders WHERE tid=%s AND uid=%s', (topic.tid, form.uid.data))
+        is_grader = bool(db.fetchone())
+
+        if form.assign.data:
+            if is_grader:
+                return "This student is already assigned", HTTPStatus.BAD_REQUEST
+            db_query('INSERT INTO owl_student_graders (tid, uid) VALUES (%s, %s)', (topic.tid, form.uid.data))
+        else:
+            if not is_grader:
+                return "This student is already not assigned", HTTPStatus.BAD_REQUEST
+            db_query('DELETE FROM owl_student_graders WHERE tid=%s AND uid=%s', (topic.tid, form.uid.data))
+
+        db_connection.commit()
+        return redirect(url_for('admin_topic_graders', cident=cident, tid=tid))
+    else:
+        db_query("""
+                SELECT s.uid, s.full_name,
+                EXISTS (SELECT * FROM owl_student_graders x WHERE x.tid=%s AND x.uid=s.uid) AS is_grader
+                FROM owl_enroll e
+                JOIN owl_users s ON s.uid = e.uid
+                WHERE e.cid = %s
+                  AND e.is_teacher = false
+                ORDER BY s.full_name
+            """, (topic.tid, g.course.cid))
+        students = [(s, AssignGraderForm(uid=s.uid)) for s in db.fetchall()]
+
+        s_unassigned = [s for s in students if not s[0].is_grader]
+        s_assigned = [s for s in students if s[0].is_grader]
+
+        return render_template('admin-topic-graders.html', s_unassigned=s_unassigned, s_assigned=s_assigned, topic=topic)
 
 
 class EditTopicForm(FlaskForm):
diff --git a/templates/admin-topic-graders.html b/templates/admin-topic-graders.html
new file mode 100644
index 0000000..11c717e
--- /dev/null
+++ b/templates/admin-topic-graders.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+{% block body %}
+	<h2>Student graders of {{ topic.title }}</h2>
+
+	<div class="buttons">
+		<span class="button"><a href="{{url_for('admin_topics', cident=g.course.ident)}}">Back</a></span>
+	</div>
+
+	<h3>Assigned graders</h3>
+	<ul>
+	{% for s, form in s_assigned %}
+	<li><form method="POST" action="">
+		{{ form.csrf_token }}
+		{{ form.uid }}
+		<b>{{ s.full_name }}</b>:
+		{{ form.unassign }}
+	</form></li>
+	{% endfor %}
+	</ul>
+
+	<h3>Unassigned students</h3>
+	<ul>
+	{% for s, form in s_unassigned %}
+	<li><form method="POST" action="">
+		{{ form.csrf_token }}
+		{{ form.uid }}
+		<b>{{ s.full_name }}</b>:
+		{{ form.assign }}
+	</form></li>
+	{% endfor %}
+	</ul>
+{% endblock %}
-- 
GitLab


From edbb69e42928127fe74fa484a1ba49d9fc095189 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Kon=C4=8Dick=C3=BD?=
 <koncicky@kam.mff.cuni.cz>
Date: Tue, 2 Mar 2021 23:24:23 +0100
Subject: [PATCH 6/7] Links converted to buttons

---
 templates/topic-grade.html | 4 +++-
 templates/topic.html       | 8 +++++---
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/templates/topic-grade.html b/templates/topic-grade.html
index 6f66481..83ec78f 100644
--- a/templates/topic-grade.html
+++ b/templates/topic-grade.html
@@ -4,7 +4,9 @@
 	{% set t=g.topic %}
 	<h2>{{c.name}}</h2>
 
-		<p><a href='{{ url_for('course_index', cident=c.ident) }}'>Return to course</a>
+		<div class="buttons">
+			<span class="button"><a href='{{ url_for('course_index', cident=c.ident) }}'>Return to course</a></span>
+		</div>
 
 	<h3>Grade {{t.title}}</h3>
 		<table class=results>
diff --git a/templates/topic.html b/templates/topic.html
index 711b5fd..d32965d 100644
--- a/templates/topic.html
+++ b/templates/topic.html
@@ -7,12 +7,14 @@
 {% endif %}
 	<h2>{{ g.course.name }}</h2>
 
-	<p><a href='{{ url_for('course_index', cident=g.course.ident) }}'>Back to the course&hellip;</a>&nbsp;&nbsp;
+	<div class="buttons">
+		<span class="button"><a href='{{ url_for('course_index', cident=g.course.ident) }}'>Back to the course</a></span>
 	{% if g.is_teacher %}
-		&nbsp;&nbsp;<a href='{{ url_for('teacher') }}'>Teacher's summary</a>
+		<span class="button"><a href='{{ url_for('teacher') }}'>Teacher's summary</a></span>
 	{% elif g.is_grader %}
-		&nbsp;&nbsp;<a href='{{ url_for('topic_student_grade', cident=g.course.ident, tident=g.topic.ident) }}'>Grade&hellip;</a>
+		<span class="button"><a href='{{ url_for('topic_student_grade', cident=g.course.ident, tident=g.topic.ident) }}'>Grade</a></span>
 	{% endif %}
+	</div>
 
 	<h3>{{ g.topic.title }}{% if not g.topic.public %} [private]{% endif %}</h3>
 
-- 
GitLab


From b6c93aae1c9ee1312eecb5a89cabf5c870b7e16d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Kon=C4=8Dick=C3=BD?=
 <koncicky@kam.mff.cuni.cz>
Date: Fri, 5 Mar 2021 20:27:22 +0100
Subject: [PATCH 7/7] Student graders also receive notifications

---
 owl.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/owl.py b/owl.py
index 5792072..6a3d277 100644
--- a/owl.py
+++ b/owl.py
@@ -1472,10 +1472,10 @@ def send_notify_post(post):
             FROM owl_enroll e
             JOIN owl_users u ON u.uid = e.uid
             WHERE e.cid = %s
-              AND (%s < 0 OR e.uid = %s OR e.is_teacher = true)
+              AND (%s < 0 OR e.uid = %s OR e.is_teacher = true OR e.uid IN (SELECT uid FROM owl_student_graders WHERE tid=%s))
               AND u.notify = true
               AND u.email IS NOT NULL
-        """, (topic.cid, post.target_uid, post.target_uid))
+        """, (topic.cid, post.target_uid, post.target_uid, topic.tid))
     dests = db.fetchall()
 
     for dest in dests:
-- 
GitLab