Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cms/db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,12 @@ class Question(Base):

MAX_SUBJECT_LENGTH = 50
MAX_TEXT_LENGTH = 2000
QUICK_ANSWERS = {
"yes": "Yes",
"no": "No",
"invalid": "Invalid Question (not a Yes/No Question)",
"nocomment": "No Comment/Please refer to task statement",
}

# Auto increment primary key.
id: int = Column(
Expand Down
4 changes: 3 additions & 1 deletion cms/server/admin/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
LoginHandler, \
LogoutHandler, \
ResourcesHandler, \
NotificationsHandler
NotificationsHandler, \
MarkdownRenderHandler
from .submission import \
SubmissionHandler, \
SubmissionCommentHandler, \
Expand Down Expand Up @@ -113,6 +114,7 @@
(r"/resources/([0-9]+|all)/([0-9]+)", ResourcesHandler),
(r"/notifications", NotificationsHandler),
(r"/file/([a-f0-9]+)/([a-zA-Z0-9_.-]+)", FileFromDigestHandler),
(r"/render_markdown", MarkdownRenderHandler),

# Contest

Expand Down
73 changes: 31 additions & 42 deletions cms/server/admin/handlers/contestquestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

"""

from abc import ABCMeta, abstractmethod
import logging

import collections
Expand Down Expand Up @@ -64,38 +65,51 @@ def get(self, contest_id):
self.render("questions.html", **self.r_params)


class QuestionReplyHandler(BaseHandler):
"""Called when the manager replies to a question made by a user.

"""
QUICK_ANSWERS = {
"yes": "Yes",
"no": "No",
"invalid": "Invalid Question (not a Yes/No Question)",
"nocomment": "No Comment/Please refer to task statement",
}
class QuestionActionHandler(BaseHandler, metaclass=ABCMeta):
"""Base class for handlers for actions on questions."""

@abstractmethod
def process_question(self, question: Question):
"""Called on POST requests. Perform the appropriate action on the
question."""
pass

@require_permission(BaseHandler.PERMISSION_MESSAGING)
def post(self, contest_id, question_id):
ref = self.url("contest", contest_id, "questions")
user_id = self.get_argument("user_id", None)
if user_id is not None:
ref = self.url("contest", contest_id, "user", user_id, "edit")
else:
ref = self.url("contest", contest_id, "questions")
Comment thread
veluca93 marked this conversation as resolved.

question = self.safe_get_item(Question, question_id)
self.contest = self.safe_get_item(Contest, contest_id)

# Protect against URLs providing incompatible parameters.
if self.contest is not question.participation.contest:
raise tornado_web.HTTPError(404)

self.process_question(question)
self.redirect(ref)

class QuestionReplyHandler(QuestionActionHandler):
"""Called when the manager replies to a question made by a user.

"""

def process_question(self, question):
reply_subject_code: str = self.get_argument(
"reply_question_quick_answer", "")
question.reply_text = self.get_argument("reply_question_text", "")

# Ignore invalid answers
if reply_subject_code not in QuestionReplyHandler.QUICK_ANSWERS:
if reply_subject_code not in Question.QUICK_ANSWERS:
question.reply_subject = ""
else:
# Quick answer given, ignore long answer.
question.reply_subject = \
QuestionReplyHandler.QUICK_ANSWERS[reply_subject_code]
Question.QUICK_ANSWERS[reply_subject_code]
question.reply_text = ""

question.reply_timestamp = make_datetime()
Expand All @@ -106,26 +120,14 @@ def post(self, contest_id, question_id):
"question with id %s.",
question.participation.user.username,
question.participation.contest.name,
question_id)

self.redirect(ref)

question.id)

class QuestionIgnoreHandler(BaseHandler):
class QuestionIgnoreHandler(QuestionActionHandler):
"""Called when the manager chooses to ignore or stop ignoring a
question.

"""
@require_permission(BaseHandler.PERMISSION_MESSAGING)
def post(self, contest_id, question_id):
ref = self.url("contest", contest_id, "questions")
question = self.safe_get_item(Question, question_id)
self.contest = self.safe_get_item(Contest, contest_id)

# Protect against URLs providing incompatible parameters.
if self.contest is not question.participation.contest:
raise tornado_web.HTTPError(404)

def process_question(self, question):
should_ignore = self.get_argument("ignore", "no") == "yes"

# Commit the change.
Expand All @@ -139,22 +141,11 @@ def post(self, contest_id, question_id):
question.participation.contest.name,
"ignored" if should_ignore else "unignored")

self.redirect(ref)


class QuestionClaimHandler(BaseHandler):
class QuestionClaimHandler(QuestionActionHandler):
"""Called when the manager chooses to claim or unclaim a question."""

@require_permission(BaseHandler.PERMISSION_MESSAGING)
def post(self, contest_id, question_id):
ref = self.url("contest", contest_id, "questions")
question = self.safe_get_item(Question, question_id)
self.contest = self.safe_get_item(Contest, contest_id)

# Protect against URLs providing incompatible parameters.
if self.contest is not question.participation.contest:
raise tornado_web.HTTPError(404)

def process_question(self, question):
# Can claim/unclaim only a question not ignored or answered.
if question.ignored or question.reply_timestamp is not None:
raise tornado_web.HTTPError(405)
Expand All @@ -174,5 +165,3 @@ def post(self, contest_id, question_id):
question.participation.contest.name,
"claimed" if should_claim else "unclaimed",
self.current_user.name)

self.redirect(ref)
11 changes: 11 additions & 0 deletions cms/server/admin/handlers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

from cms import ServiceCoord, get_service_shards, get_service_address
from cms.db import Admin, Contest, Question
from cms.server.jinja2_toolbox import markdown_filter
from cmscommon.crypto import validate_password
from cmscommon.datetime import make_datetime, make_timestamp
from .base import BaseHandler, SimpleHandler, require_permission
Expand Down Expand Up @@ -167,3 +168,13 @@ def get(self):
self.service.notifications = []

self.write(json.dumps(res))

class MarkdownRenderHandler(BaseHandler):
"""Renders Markdown for AWS message previews."""

@require_permission(BaseHandler.AUTHENTICATED)
def post(self):
data = self.get_argument("input")
rendered = markdown_filter(data)
self.write(rendered)

2 changes: 2 additions & 0 deletions cms/server/admin/jinja2_toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from jinja2 import Environment, PackageLoader

from cms.db.user import Question
from cms.grading.languagemanager import LANGUAGES
from cms.grading.scoretypes import SCORE_TYPES
from cms.grading.tasktypes import TASK_TYPES
Expand All @@ -47,6 +48,7 @@ def instrument_cms_toolbox(env: Environment):
env.globals["LANGUAGES"] = LANGUAGES
env.globals["get_hex_random_key"] = get_hex_random_key
env.globals["parse_authentication"] = safe_parse_authentication
env.globals["question_quick_answers"] = Question.QUICK_ANSWERS


def instrument_formatting_toolbox(env: Environment):
Expand Down
6 changes: 5 additions & 1 deletion cms/server/admin/static/aws_style.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ strong, .bold {
font-weight: bold;
}

em {
font-style: italic;
}

.login_error {
color: #F31313;
}
Expand Down Expand Up @@ -420,7 +424,7 @@ th .column-sort {
text-decoration: underline;
}

.reply_question textarea {
.markdown_input {
width: 100%;
}

Expand Down
49 changes: 49 additions & 0 deletions cms/server/admin/static/aws_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -826,3 +826,52 @@ CMS.AWSUtils.ajax_delete = function(url) {
CMS.AWSUtils.ajax_post = function(url) {
CMS.AWSUtils.ajax_edit_request("POST", url);
};


/**
* Used by templates/macro/question.html.
* Toggles visibility of the question reply box.
*/
CMS.AWSUtils.prototype.question_reply_toggle = function(event, invoker) {
var obj = invoker.parentElement.parentElement.querySelector(".reply_question");
if (obj.style.display != "block") {
obj.style.display = "block";
invoker.innerHTML = "Hide reply";
} else {
obj.style.display = "none";
invoker.innerHTML = "Reply";
}
event.preventDefault();
}

/**
* Used by templates/macro/question.html.
* Updates visibility of answer box when choosing quick answers.
*/
CMS.AWSUtils.prototype.update_additional_answer = function(event, invoker) {
var obj = $(invoker).parent().find(".alternative_answer");
if (invoker.value == "other") {
obj.css("display", "");
} else {
obj.css("display", "none");
}
}

/**
* Used by templates/macro/markdown_input.html.
* Asks the server to render the markdown input and displays it.
*/
CMS.AWSUtils.prototype.render_markdown_preview = function(target) {
var form_element = $(target).closest("form");
var md_text = form_element.find(".markdown_input").val();
$.ajax({
type: "POST",
url: this.url("render_markdown"),
data: {input: md_text},
dataType: "text",
headers: {"X-XSRFToken": get_cookie("_xsrf")},
success: function(response) {
form_element.find(".markdown_preview").html(response);
},
});
}
8 changes: 5 additions & 3 deletions cms/server/admin/templates/announcements.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% import 'macro/markdown_input.html' as macro_markdown %}

{% block core %}
<div class="core_title">
Expand All @@ -23,11 +24,12 @@ <h2 id="title_announcements" class="toggling_on">Announcements</h2>
</datalist>
</div>
<div class="notification_text">
<label for="new_text">Text</label>
<textarea name="text" id="new_text" style="width: 100%" ></textarea>
<label for="text">Text</label>
{{ macro_markdown.markdown_input("text") }}
</div>
<input type="submit"
value="Add announcement" />
{{ macro_markdown.markdown_preview() }}
</form>
</div>
</div>
Expand All @@ -42,7 +44,7 @@ <h2 id="title_announcements" class="toggling_on">Announcements</h2>
</div>
<div class="notification_timestamp">{{ msg.timestamp }}</div>
<div class="notification_subject">{{ msg.subject }}</div>
<div class="notification_text preserve_line_breaks">{{ msg.text }}</div>
<div class="notification_text">{{ msg.text | markdown }}</div>
<hr>
<div class="notification_admin_owner">
By: {{ msg.admin.name if msg.admin is not none else "<unknown>" }}
Expand Down
9 changes: 9 additions & 0 deletions cms/server/admin/templates/macro/markdown_input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% macro markdown_input(name) %}
<textarea name="{{ name }}" class="markdown_input"></textarea><br/>
You can use Markdown here. URLs are not automatically linked, surround them with &lt;&gt; like &lt;http://example.org&gt;.
<div class="markdown_preview"></div>
{% endmacro %}

{% macro markdown_preview() %}
<input type="button" value="Preview Markdown" onclick="utils.render_markdown_preview(this)" />
{% endmacro %}
Loading