Skip to content

Commit 9ff9a76

Browse files
prandlaveluca93
authored andcommitted
Refactor question-answering in AWS
* Deduplicate code in questions view / participant view * Deduplicate set of quick answers As a result, the question-answering experience in the participant view is now a bit better.
1 parent 72fbc17 commit 9ff9a76

7 files changed

Lines changed: 167 additions & 189 deletions

File tree

cms/db/user.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,12 @@ class Question(Base):
336336

337337
MAX_SUBJECT_LENGTH = 50
338338
MAX_TEXT_LENGTH = 2000
339+
QUICK_ANSWERS = {
340+
"yes": "Yes",
341+
"no": "No",
342+
"invalid": "Invalid Question (not a Yes/No Question)",
343+
"nocomment": "No Comment/Please refer to task statement",
344+
}
339345

340346
# Auto increment primary key.
341347
id: int = Column(

cms/server/admin/handlers/contestquestion.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,14 @@ class QuestionReplyHandler(BaseHandler):
6868
"""Called when the manager replies to a question made by a user.
6969
7070
"""
71-
QUICK_ANSWERS = {
72-
"yes": "Yes",
73-
"no": "No",
74-
"invalid": "Invalid Question (not a Yes/No Question)",
75-
"nocomment": "No Comment/Please refer to task statement",
76-
}
7771

7872
@require_permission(BaseHandler.PERMISSION_MESSAGING)
7973
def post(self, contest_id, question_id):
80-
ref = self.url("contest", contest_id, "questions")
74+
userid = self.get_argument("user_id", None)
75+
if userid is not None:
76+
ref = self.url("contest", contest_id, "user", userid, "edit")
77+
else:
78+
ref = self.url("contest", contest_id, "questions")
8179
question = self.safe_get_item(Question, question_id)
8280
self.contest = self.safe_get_item(Contest, contest_id)
8381

@@ -90,12 +88,12 @@ def post(self, contest_id, question_id):
9088
question.reply_text = self.get_argument("reply_question_text", "")
9189

9290
# Ignore invalid answers
93-
if reply_subject_code not in QuestionReplyHandler.QUICK_ANSWERS:
91+
if reply_subject_code not in Question.QUICK_ANSWERS:
9492
question.reply_subject = ""
9593
else:
9694
# Quick answer given, ignore long answer.
9795
question.reply_subject = \
98-
QuestionReplyHandler.QUICK_ANSWERS[reply_subject_code]
96+
Question.QUICK_ANSWERS[reply_subject_code]
9997
question.reply_text = ""
10098

10199
question.reply_timestamp = make_datetime()
@@ -118,7 +116,11 @@ class QuestionIgnoreHandler(BaseHandler):
118116
"""
119117
@require_permission(BaseHandler.PERMISSION_MESSAGING)
120118
def post(self, contest_id, question_id):
121-
ref = self.url("contest", contest_id, "questions")
119+
userid = self.get_argument("user_id", None)
120+
if userid is not None:
121+
ref = self.url("contest", contest_id, "user", userid, "edit")
122+
else:
123+
ref = self.url("contest", contest_id, "questions")
122124
question = self.safe_get_item(Question, question_id)
123125
self.contest = self.safe_get_item(Contest, contest_id)
124126

@@ -147,7 +149,11 @@ class QuestionClaimHandler(BaseHandler):
147149

148150
@require_permission(BaseHandler.PERMISSION_MESSAGING)
149151
def post(self, contest_id, question_id):
150-
ref = self.url("contest", contest_id, "questions")
152+
userid = self.get_argument("user_id", None)
153+
if userid is not None:
154+
ref = self.url("contest", contest_id, "user", userid, "edit")
155+
else:
156+
ref = self.url("contest", contest_id, "questions")
151157
question = self.safe_get_item(Question, question_id)
152158
self.contest = self.safe_get_item(Contest, contest_id)
153159

cms/server/admin/jinja2_toolbox.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from jinja2 import Environment, PackageLoader
2727

28+
from cms.db.user import Question
2829
from cms.grading.languagemanager import LANGUAGES
2930
from cms.grading.scoretypes import SCORE_TYPES
3031
from cms.grading.tasktypes import TASK_TYPES
@@ -47,6 +48,7 @@ def instrument_cms_toolbox(env: Environment):
4748
env.globals["LANGUAGES"] = LANGUAGES
4849
env.globals["get_hex_random_key"] = get_hex_random_key
4950
env.globals["parse_authentication"] = safe_parse_authentication
51+
env.globals["question_quick_answers"] = Question.QUICK_ANSWERS
5052

5153

5254
def instrument_formatting_toolbox(env: Environment):

cms/server/admin/static/aws_utils.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,3 +826,33 @@ CMS.AWSUtils.ajax_delete = function(url) {
826826
CMS.AWSUtils.ajax_post = function(url) {
827827
CMS.AWSUtils.ajax_edit_request("POST", url);
828828
};
829+
830+
831+
/**
832+
* Used by templates/macro/question.html.
833+
* Toggles visibility of the question reply box.
834+
*/
835+
CMS.AWSUtils.prototype.question_reply_toggle = function(event, invoker) {
836+
var obj = invoker.parentElement.parentElement.querySelector(".reply_question");
837+
if (obj.style.display != "block") {
838+
obj.style.display = "block";
839+
invoker.innerHTML = "Hide reply";
840+
} else {
841+
obj.style.display = "none";
842+
invoker.innerHTML = "Reply";
843+
}
844+
event.preventDefault();
845+
}
846+
847+
/**
848+
* Used by templates/macro/question.html.
849+
* Updates visibility of answer box when choosing quick answers.
850+
*/
851+
CMS.AWSUtils.prototype.update_additional_answer = function(event, invoker) {
852+
var obj = invoker.parentElement.querySelector(".alternative_answer");
853+
if (invoker.value == "other") {
854+
obj.style.display = "block";
855+
} else {
856+
obj.style.display = "none";
857+
}
858+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
{# This macro should be imported "with context". #}
2+
3+
{% macro question(msg, user_id) %}
4+
{#
5+
Render one question by a participant.
6+
7+
msg (Question): the question object.
8+
user_id (int|None): if we are rendering a single user's questions, this is the
9+
user's id. if we are rendering all questions of the contest, then None.
10+
#}
11+
<div class="notification communication {{ ("answered" if msg.reply_timestamp is not none else ("ignored" if msg.ignored else "")) }}">
12+
<div class="notification_msg">
13+
<div class="notification_timestamp">
14+
{% if user_id is none %}
15+
<a href="{{ url("contest", contest.id, "user", msg.participation.user.id, "edit") }}" title="{{ msg.participation.user.first_name }} {{ msg.participation.user.last_name }}">{{ msg.participation.user.username }}</a> &mdash;
16+
{% endif %}
17+
{{ msg.question_timestamp }}
18+
</div>
19+
<div class="notification_subject">{{ msg.subject }}</div>
20+
<div class="notification_text preserve_line_breaks">{{ msg.text }}</div>
21+
{% if msg.reply_timestamp is not none %}
22+
<hr>
23+
<div class="notification_subject">Reply: {{ msg.reply_subject }}</div>
24+
<div class="notification_text preserve_line_breaks">{{ msg.reply_text }}</div>
25+
<hr>
26+
<div class="notification_admin_owner">
27+
Reply from: {{ msg.admin.name if msg.admin is not none else "<unknown>" }}</div>
28+
{% else %}
29+
<hr>
30+
<div class="notification_subject">Not yet replied.</div>
31+
<hr>
32+
{% if msg.admin is not none %}
33+
<div class="notification_admin_owner">
34+
{{ "Ignored" if msg.ignored else "Claimed" }} by: {{ msg.admin.name }}
35+
</div>
36+
{% endif %}
37+
{% endif %}
38+
{% if admin.permission_all or admin.permission_messaging %}
39+
{% if msg.reply_timestamp is none %}
40+
{% if not msg.ignored %}
41+
<div class="claim_reply">
42+
<form class="claim_question_form" action="{{ url("contest", contest.id, "question", msg.id, "claim") }}" name="claim{{ msg.id }}" method="POST">
43+
{{ xsrf_form_html|safe }}
44+
{% if user_id is not none %}
45+
<input type="hidden" name="user_id" value="{{ user_id }}" />
46+
{% endif %}
47+
{% if msg.admin is none %}
48+
<input type="hidden" name="claim" value="yes"/>
49+
<a href="javascript:void(0);" onclick="document.claim{{ msg.id }}.submit();">Claim</a>
50+
{% else %}
51+
<input type="hidden" name="claim" value="no"/>
52+
<a href="javascript:void(0);" onclick="document.claim{{ msg.id }}.submit();">Unclaim</a>
53+
{% endif %}
54+
</form>
55+
</div>
56+
{% endif %}
57+
<div class="ignore_reply">
58+
<form class="ignore_question_form" action="{{ url("contest", contest.id, "question", msg.id, "ignore") }}" name="ignore{{ msg.id }}" method="POST">
59+
{{ xsrf_form_html|safe }}
60+
{% if user_id is not none %}
61+
<input type="hidden" name="user_id" value="{{ user_id }}" />
62+
{% endif %}
63+
{% if not msg.ignored %}
64+
<input type="hidden" name="ignore" value="yes"/>
65+
<a href="javascript:void(0);" onclick="document.ignore{{ msg.id }}.submit();">Ignore</a>
66+
{% else %}
67+
<input type="hidden" name="ignore" value="no"/>
68+
<a href="javascript:void(0);" onclick="document.ignore{{ msg.id }}.submit();">Unignore</a>
69+
{% endif %}
70+
</form>
71+
</div>
72+
{% endif %}
73+
<div class="reply_question_toggle">
74+
<a href="javascript:void(0);" onclick="utils.question_reply_toggle(event, this);">
75+
{% if msg.reply_timestamp is none %}
76+
Reply
77+
{% else %}
78+
Edit reply
79+
{% endif %}
80+
</a>
81+
</div>
82+
<div class="reply_question">
83+
<hr/>
84+
<form class="reply_question_form" action="{{ url("contest", contest.id, "question", msg.id, "reply") }}" method="POST">
85+
{{ xsrf_form_html|safe }}
86+
{% if user_id is not none %}
87+
<input type="hidden" name="user_id" value="{{ user_id }}" />
88+
{% endif %}
89+
Precompiled answer:
90+
<select name="reply_question_quick_answer" onchange="utils.update_additional_answer(event, this);">
91+
{% for key, value in question_quick_answers.items() %}
92+
<option value="{{ key }}">{{ value }}</option>
93+
{% endfor %}
94+
<option value="other" selected>Other</option>
95+
</select>
96+
<br/>
97+
<div class="alternative_answer">
98+
Alternative answer:<br/>
99+
<textarea name="reply_question_text"></textarea><br/>
100+
You can use Markdown here. URLs are not automatically linked, surround them with &lt;&gt; like &lt;http://example.org&gt;.
101+
</div>
102+
<input type="submit" value="Send"/>
103+
</form>
104+
</div>
105+
{% endif %}
106+
</div>
107+
</div>
108+
{% endmacro %}

cms/server/admin/templates/participation.html

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,7 @@
22

33
{% extends "base.html" %}
44
{% import 'macro/submission.html' as macro_submission %}
5-
6-
{% block js %}
7-
function question_reply_toggle(element, invoker)
8-
{
9-
var obj = document.getElementsByClassName("reply_question")[element];
10-
if (obj.style.display != "block")
11-
{
12-
obj.style.display = "block";
13-
invoker.innerHTML = "Hide reply";
14-
}
15-
else
16-
{
17-
obj.style.display = "none";
18-
invoker.innerHTML = "Reply";
19-
}
20-
return false;
21-
}
22-
23-
function update_additional_answer(element, invoker)
24-
{
25-
var obj = document.getElementsByClassName("alternative_answer")[element];
26-
if (invoker.selectedIndex == 5)
27-
obj.style.display = "block";
28-
else
29-
obj.style.display = "none";
30-
}
31-
{% endblock js %}
5+
{% import 'macro/question.html' as macro_question with context %}
326

337
{% block core %}
348
<h1>
@@ -148,44 +122,7 @@ <h2 id="title_questions" class="toggling_on">Questions</h2>
148122
{% if participation.questions != [] %}
149123
<div class="notifications">
150124
{% for msg in participation.questions %}
151-
<div class="notification communication">
152-
<div class="notification_msg">
153-
<div class="notification_timestamp">{{ msg.question_timestamp }}</div>
154-
<div class="notification_subject">{{ msg.subject }}</div>
155-
<div class="notification_text preserve_line_breaks">{{ msg.text }}</div>
156-
{% if msg.reply_timestamp is not none %}
157-
<div class="notification_subject">Reply. {{ msg.reply_subject }}</div>
158-
<div class="notification_text preserve_line_breaks">{{ msg.reply_text }}</div>
159-
{% else %}
160-
<div class="notification_subject">Not yet replied.</div>
161-
{% endif %}
162-
<div class="reply_question_toggle">
163-
<a href="javascript:void(0);" onclick="return question_reply_toggle({{ loop.index0 }}, this);">Reply</a>
164-
</div>
165-
<div class="reply_question" >
166-
<hr/>
167-
<form class="reply_question_form" action="{{ url("contest", contest.id, "question", msg.id, "reply") }}" method="POST">
168-
{{ xsrf_form_html|safe }}
169-
<input type="hidden" name="ref" value="/contest/{{ contest.id }}/user/{{ selected_user.id }}/edit"/>
170-
Precompiled answer:
171-
<select name="reply_question_quick_answer" onchange="update_additional_answer({{ loop.index0 }}, this);">
172-
<option value="yes">Yes</option>
173-
<option value="no">No</option>
174-
<option value="invalid">Invalid Question (not a Yes/No Question)</option>
175-
<option value="nocomment">No Comment/Please refer to task statement</option>
176-
<option selected value="other">Other</option>
177-
</select>
178-
<br/>
179-
<div class="alternative_answer">
180-
Alternative answer:<br/>
181-
<textarea name="reply_question_text"></textarea><br/>
182-
You can use Markdown here. URLs are not automatically linked, surround them with &lt;&gt; like &lt;http://example.org&gt;.
183-
</div>
184-
<input type="submit" value="Send"/>
185-
</form>
186-
</div>
187-
</div>
188-
</div>
125+
{{ macro_question.question(msg, selected_user.id) }}
189126
{% endfor %}
190127
</div>
191128

0 commit comments

Comments
 (0)