Skip to content

Commit

Permalink
Announcements and Clarifications (#12)
Browse files Browse the repository at this point in the history
* SQL models for Announcements and Clarifications

* Verify the max length

* Frontend template for admin/announcements

* Implement admin/announcement endpoints

* Implement contestant's view announcements page

* Clars have updated_at, not created_at

* Add shortcuts for links

* Implement endpoints for admin/clarifications

* Convert user UI announcements to merged Messaged

* Change userID to string

* Send clarification form

* Reply clarifications form

* No clarifications after the contest

* Fix formatting

* Merge undone JS stuff

* Implement updates/notifications for user/messages

* Fix copy typo

* Some visual tweaks

* Simple clarification counter for admin panel

* Manage contest IDs in state

* Add some announcements and clarifications to test data
  • Loading branch information
natsukagami authored Apr 22, 2020
1 parent 26704fa commit 1c4932f
Show file tree
Hide file tree
Showing 27 changed files with 1,082 additions and 16 deletions.
35 changes: 35 additions & 0 deletions assets/sql/v6.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
BEGIN TRANSACTION;

-- An "Announcements" table.
CREATE TABLE announcements (
id INTEGER NOT NULL PRIMARY KEY,
contest_id INTEGER NOT NULL,
problem_id INTEGER,
content BLOB NOT NULL,
created_at DATETIME NOT NULL,

FOREIGN KEY (contest_id) REFERENCES contests(id) ON DELETE CASCADE,
FOREIGN KEY (problem_id) REFERENCES problems(id) ON DELETE CASCADE
);
CREATE INDEX announcements_by_contest ON announcements(contest_id ASC, id DESC);

-- Clarifications table.
CREATE TABLE clarifications (
id INTEGER NOT NULL PRIMARY KEY,
user_id VARCHAR NOT NULL,
contest_id INTEGER NOT NULL,
problem_id INTEGER,

content BLOB NOT NULL,
updated_at DATETIME NOT NULL, -- Updated when responded

response BLOB,

FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (contest_id) REFERENCES contests(id) ON DELETE CASCADE,
FOREIGN KEY (problem_id) REFERENCES problems(id) ON DELETE CASCADE
);

CREATE INDEX clarifications_by_user ON clarifications(contest_id ASC, user_id ASC, id DESC);

COMMIT;
80 changes: 80 additions & 0 deletions frontend/html/admin/clarifications.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{{ define "admin-title" }}Clarifications{{ end }}

{{ define "admin-content" }}

<div class="py-4 mx-auto text-4xl">Clarifications</div>

<div class="p-2">
<div class="my-2">
<input id="show-unanswered" type="checkbox">
<label for="show-unanswered">Show only unanswered clarifications.</label>
</div>
{{ range .Clarifications }}
<div class="mt-2 p-2 bg-gray-200 rounded border hover:bg-blue-200 clarification" data-answered="{{.Responded}}">
<div class="flex flex-row justify-between">
<div>
<div class="text-sm">
{{ with (index $.Contests .ContestID) }}
{{ $contestLink := .AdminLink }}
Contest:
<a href="{{$contestLink}}" class="font-semibold hover:text-blue-600">{{.Name}}</a>
{{ end }}
</div>
<div class="text-2xl">
{{ if .ProblemID.Valid }}
{{ with (index $.Problems .ProblemID.Int64) }}
{{ $problemLink := .AdminLink }}
re:
<a href="{{$problemLink}}" class="font-semibold hover:text-blue-600">
{{.Name}}. {{.DisplayName}}
</a>
{{ end }}
{{ else }}
General Question
{{ end }}
</div>
</div>
<div class="text-right">
<div class="text-sm italic display-time" data-time="{{.UpdatedAt | time}}"></div>
<div>By:
{{ $user_link := printf "/admin/users/%s" .UserID }}
<a href="{{$user_link}}" class="font-semibold hover:text-blue-600">{{.UserID}}</a>
</div>
</div>
</div>
<pre class="whitespace-pre-wrap m-2">{{- printf "%s" .Content -}}</pre>
{{ if .Response }}
<div>
<div class="text-lg">Response:</div>
<pre class="whitespace-pre-wrap mt-1 mx-2">{{- printf "%s" .Response -}}</pre>
</div>
{{ else }}
<form class="form-block" method="POST" action="{{.AdminLink}}">
<label for="response" class="text-sm block">Response</label>
<textarea class="form-input text-lg overflow-y-auto whitespace-pre-wrap leading-relaxed h-20"
name="response" maxlength="2048" required></textarea>

<label for="premade" class="text-sm block">Template Answers</label>
<select class="form-input premade">
<option value="" selected>None</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
<option value="Read problem description">Read problem description</option>
<option value="No Answer">No Answer</option>
</select>

<div class="mt-2">
<input required type="submit" class="form-btn bg-green-200 hover:bg-green-300" value="Submit">
<input required type="reset" class="form-btn bg-red-200 hover:bg-red-300" value="Reset">
</div>
</form>
{{ end }}
</div>
<div id="no-unanswered" class="hidden text-center text-lg">No unanswered Clarifications, great!</div>
{{ else }}
<div class="text-center text-lg">No Clarifications</div>
{{ end }}
<script src="../../ts/admin_clarifications.ts"></script>
</div>

{{ end }}
2 changes: 2 additions & 0 deletions frontend/html/admin/contest.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
{{ $contest_link := printf "/admin/contests/%d" .Contest.ID }}
<div id="overview" class="py-4 mx-auto text-4xl">{{.Name}}
<span class="text-2xl">(
<a href="{{$contest_link}}/announcements" title="View contest's announcements"
class="hover:text-blue-600 cursor-pointer">Announcements</a> |
<a href="{{$contest_link}}/scoreboard" title="View contest's scoreboard"
class="hover:text-blue-600 cursor-pointer">Scoreboard</a> |
<a href="{{$contest_link}}/submissions" title="See submissions for contest"
Expand Down
80 changes: 80 additions & 0 deletions frontend/html/admin/contest_announcements.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{{ define "admin-title" }}Announcements - {{.Contest.Name}}{{ end }}

{{ define "admin-nav" }}
<nav>
<a href="#new">
<div class="bg-gray-200 rounded-sm hover:bg-gray-400 m-2 py-1 ml-2 pl-4">New Announcement</div>
</a>
<a href="#list">
<div class="bg-gray-200 rounded-sm hover:bg-gray-400 m-2 py-1 ml-4 pl-4">Past Announcements</div>
</a>
</nav>
{{ end }}

{{ define "admin-content" }}
{{ $contest_link := printf "/admin/contests/%d" .Contest.ID }}
<div class="py-4 mx-auto">
<a class="text-3xl text-gray-600 hover:text-blue-600 cursor-pointer" href="{{$contest_link}}">
{{.Contest.Name}}
</a>
<span>>></span>
<span class="text-4xl">Announcements</span>
</div>

<div class="subheader" id="new">New Announcement</div>
{{ template "form-error" .Error }}
<form method="POST" action="{{$contest_link}}/announcements" class="form-block">
<label for="problem" class="text-sm block">For problem</label>
<select class="form-input" id="problem" name="problem">
{{ if eq .Form.Problem 0 }}
<option selected value="0">General Annonuncement</option>
{{ else }}
<option value="0">General Annonuncement</option>
{{ end }}
{{ range .Problems }}
{{ if eq $.Form.Problem .ID }}
<option selected value="{{.ID}}">{{.Name}}. {{.DisplayName}}</option>
{{ else }}
<option value="{{.ID}}">{{.Name}}. {{.DisplayName}}</option>
{{ end }}
{{ end }}
</select>

<label for="content" class="text-sm block">Content</label>
<textarea id="content" class="form-input text-lg overflow-y-auto whitespace-pre-wrap leading-relaxed h-40"
name="content" maxlength="2048" required>{{.Form.Content}}</textarea>

<div class="mt-2">
<input required type="submit" class="form-btn bg-green-200 hover:bg-green-300" value="Submit">
<input required type="reset" class="form-btn bg-red-200 hover:bg-red-300" value="Reset">
</div>
</form>

<div class="subheader" id="list">Past Announcements</div>
<div class="p-2">
{{ range .Announcements }}
<div class="p-2 border rounded-sm bg-gray-200 hover:bg-blue-200 my-2">
<div class="flex flex-row justify-between">
<div class="text-2xl">
{{ if .ProblemID.Valid }}
{{ with (index $.Problems .ProblemID.Int64) }}
{{ $problem_link := printf "/admin/problems/%d" .ID }}
Problem
<a href="{{$problem_link}}" class="font-semibold hover:text-blue-600">{{.Name}}. {{.DisplayName}}</a>
{{ end }}
{{ else }}
<span class="font-semibold">General Announcement</span>
{{ end }}
</div>
<div class="text-sm italic display-time" data-time="{{.CreatedAt | time}}"></div>
</div>
<pre class="mt-2 overflow-y-auto whitespace-pre-wrap">
{{- printf "%s" .Content -}}
</pre>
</div>
{{ else }}
<div class="text-center">No announcements</div>
{{ end }}
</div>

{{ end }}
2 changes: 2 additions & 0 deletions frontend/html/admin/contest_inputs.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
<td class="py-2 text-center display-time" data-time="{{.StartTime | time}}">{{.StartTime}}</td>
<td class="py-2 text-center display-time" data-time="{{.EndTime | time}}">{{.EndTime}}</td>
<td class="py-2 text-center">
<a href="{{$link}}/announcements" title="Announcements of the contest"
class="text-btn hover:text-green-600">[a]</a>
<a href="{{$link}}/submissions" title="See submissions for contest"
class="text-btn hover:text-green-600">[s]</a>
<a href="{{$link}}/scoreboard" title="View contest's scoreboard"
Expand Down
7 changes: 7 additions & 0 deletions frontend/html/admin/root.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
<a href="/admin/contests">
<div class="bg-gray-300 rounded-sm hover:bg-gray-400 m-2 py-2 pl-4">Contests</div>
</a>
<a href="/admin/clarifications">
<div class="bg-gray-300 rounded-sm hover:bg-gray-400 m-2 py-2 px-4 flex flex-row justify-between">
<div>Clarifications</div>
<div class="rounded-full bg-red-600 text-white font-bold px-2 hidden" id="unanswered-counter"></div>
</div>
</a>
<a href="/admin/users">
<div class="bg-gray-300 rounded-sm hover:bg-gray-400 m-2 py-2 pl-4">Users</div>
</a>
Expand All @@ -33,5 +39,6 @@
<div class="flex-grow overflow-auto border-l px-4 mb-2">
{{ block "admin-content" . }}{{ end }}
</div>
<script src="../../ts/admin.ts"></script>
</div>
{{ end }}
130 changes: 130 additions & 0 deletions frontend/html/contests/messages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{{ define "inner-title" }}Messages{{ end }}

{{ define "content" }}
<div class="text-4xl py-4"><b>{{.Contest.Name}}</b>: Messages</div>

<div class="text-xl my-2 text-gray-800 timer" data-start="{{.Contest.StartTime | time}}"
data-end="{{.Contest.EndTime | time}}"><span class="font-semibold"></span></div>

{{ if isFuture .Contest.EndTime }}
<a class="text-lg inline-block rounded p-2 bg-green-200 hover:bg-green-300" href="#send-clarification">Request a
Clarification</a>
{{ end }}

<div class="subheader">Messages</div>
<div class="p-2">
{{ range .Messages }}
{{ with .Announcement }}
{{ template "announcement" (zip . $) }}
{{ end }}
{{ with .Clarification }}
{{ template "clarification" (zip . $) }}
{{ end }}
{{ else }}
<div class="text-center">No messages</div>
{{ end }}
</div>

{{ if isFuture .Contest.EndTime }}
<div class="subheader" id="send-clarification">Request a Clarification</div>
{{ template "form-error" .FormError }}
{{ template "send-clarification" . }}
{{ end }}

<script>
window.announcements.markUnread();
window.announcements.setLast();
</script>

{{ end }}

{{ define "send-clarification" }}
{{ $form_link := printf "/contests/%d/messages" .Contest.ID }}
<form class="form-block" method="POST" action="{{$form_link}}">
<label for="problem" class="text-sm block">For problem</label>
<select class="form-input" id="problem" name="problem">
{{ if eq .Form.Problem 0 }}
<option selected value="0">General Question</option>
{{ else }}
<option value="0">General Question</option>
{{ end }}
{{ range .Problems }}
{{ if eq $.Form.Problem .ID }}
<option selected value="{{.ID}}">{{.Name}}. {{.DisplayName}}</option>
{{ else }}
<option value="{{.ID}}">{{.Name}}. {{.DisplayName}}</option>
{{ end }}
{{ end }}
</select>

<label for="content" class="text-sm block">Content</label>
<textarea id="content" class="form-input text-lg overflow-y-auto whitespace-pre-wrap leading-relaxed h-40"
name="content" maxlength="2048" required>{{.Form.Content}}</textarea>

<div class="mt-2">
<input required type="submit" class="form-btn bg-green-200 hover:bg-green-300" value="Submit">
<input required type="reset" class="form-btn bg-red-200 hover:bg-red-300" value="Reset">
</div>
</form>
{{ end }}

{{ define "clarification" }}
{{ with (index . 0) }}
<div class="mt-2 p-2 {{if .Responded}}bg-gray-200{{else}}bg-red-200{{end}} rounded border hover:bg-blue-200 clarification"
data-id="{{.ID}}" data-responded="{{.Responded}}">
<div class="flex flex-row justify-between">
<div>
<div class="text-2xl">
{{ if .ProblemID.Valid }}
{{ with (index (index $ 1).ProblemsMap .ProblemID.Int64) }}
{{ $problem_link := .Link }}
re:
<a href="{{$problem_link}}" class="font-semibold hover:text-blue-600">
{{.Name}}. {{.DisplayName}}
</a>
{{ end }}
{{ else }}
General Question
{{ end }}
</div>
</div>
<div class="text-right">
<div class="text-sm italic display-time" data-time="{{.UpdatedAt | time}}"></div>
</div>
</div>
<pre class="whitespace-pre-wrap m-2">{{- printf "%s" .Content -}}</pre>
{{ if .Response }}
<div>
<div class="text-lg">Response:</div>
<pre class="whitespace-pre-wrap mt-1 mx-2">{{- printf "%s" .Response -}}</pre>
</div>
{{ else }}
<div class="mt-1 text-bold">No responses yet.</div>
{{ end }}
</div>
{{ end }}
{{ end }}

{{ define "announcement" }}
{{ with (index . 0) }}
<div class="mt-2 p-2 border rounded-sm bg-gray-200 hover:bg-blue-200 my-2 announcement" data-id="{{.ID}}">
<div class="flex flex-row justify-between">
<div class="text-2xl">
{{ if .ProblemID.Valid }}
{{ with (index (index $ 1).ProblemsMap .ProblemID.Int64) }}
Problem
{{ $problem_link := .Link }}
<a href="{{$problem_link}}" class="font-semibold hover:text-blue-600">{{.Name}}. {{.DisplayName}}</a>
{{ end }}
{{ else }}
<span class="font-semibold">General Announcement</span>
{{ end }}
</div>
<div class="text-sm italic display-time" data-time="{{.CreatedAt | time}}"></div>
</div>
<pre class="mt-2 mx-2 overflow-y-auto whitespace-pre-wrap">
{{- printf "%s" .Content -}}
</pre>
</div>
{{ end }}
{{ end }}
7 changes: 7 additions & 0 deletions frontend/html/contests/root.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
<a href="{{$contest_link}}">
<div class="bg-gray-300 rounded-sm hover:bg-gray-400 m-2 py-2 pl-4">Overview</div>
</a>
<a href="{{$contest_link}}/messages">
<div class="bg-gray-300 rounded-sm hover:bg-gray-400 m-2 py-2 px-4 flex flex-row justify-between">
<div>Messages</div>
<div class="rounded-full bg-red-600 text-white font-bold px-2 hidden" id="messages-counter"></div>
</div>
</a>
<script src="../../ts/announcements.ts"></script>
<a href="{{$contest_link}}/scoreboard">
<div class="bg-gray-300 rounded-sm hover:bg-gray-400 m-2 py-2 pl-4">Scoreboard</div>
</a>
Expand Down
Binary file added frontend/sounds/notification.ogg
Binary file not shown.
Loading

0 comments on commit 1c4932f

Please sign in to comment.