Skip to content

Commit

Permalink
Add submission activity graph to users page; DMOJ#236
Browse files Browse the repository at this point in the history
  • Loading branch information
Ninjaclasher committed Aug 11, 2020
1 parent 6619af2 commit 5248d02
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 4 deletions.
22 changes: 21 additions & 1 deletion judge/views/user.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import itertools
import json
import os
from datetime import datetime
from datetime import date, datetime
from operator import attrgetter, itemgetter

from django.conf import settings
Expand All @@ -15,6 +15,8 @@
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import transaction
from django.db.models import Count, Max, Min
from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractYear
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
Expand Down Expand Up @@ -182,6 +184,24 @@ def get_context_data(self, **kwargs):
ratio = (max_ever - max_user) / (max_ever - min_ever) if max_ever != min_ever else 1.0
context['max_graph'] = max_user + ratio * delta
context['min_graph'] = min_user + ratio * delta - delta

submissions = (
self.object.submission_set
.annotate(date_only=Cast('date', DateField()))
.values('date_only').annotate(cnt=Count('id'))
)

context['submission_data'] = mark_safe(json.dumps({
date_counts['date_only'].isoformat(): date_counts['cnt'] for date_counts in submissions
}))
context['submission_metadata'] = mark_safe(json.dumps({
'month_names': {index: date_format(date(1970, index + 1, 1), 'M') for index in range(12)},
'min_year': (
self.object.submission_set
.annotate(year_only=ExtractYear('date'))
.aggregate(min_year=Min('year_only'))['min_year']
),
}))
return context


Expand Down
2 changes: 0 additions & 2 deletions resources/table.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
@import "vars";

$table_header_rounding: 6px;

.h-scrollable-table {
overflow-x: auto;
}
Expand Down
99 changes: 99 additions & 0 deletions resources/users.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import "vars";

#content-left {
&.users {
flex: unset;
Expand Down Expand Up @@ -263,3 +265,100 @@ a.edit-profile {
color: white;
}
}

#submission-activity {
#submission-activity-actions {
text-align: center;
#prev-year-action, #next-year-action {
font-size: 1.75em;
}
#year {
font-size: 1.25em;
color: #444;
}
}

#submission-activity-display {
border: 1px solid $border_gray;
border-radius: $table_header_rounding;

.info-bar {
display: flex;
justify-content: space-between;

.info-table {
width: 15%;
min-width: 130px;

.info-table-text {
width: 8%;
}
}
}

.info-text {
font-size: 0.75em;
line-height: 1;
font-weight: 100;
color: #444;
}

#submission-total-count {
align-self: center;
padding-left: 8%;
font-size: 0.85em;
}

@media(max-width: 1000px) {
#submission-total-count {
padding-left: 5px;
}
}

table {
width: 100%;
padding: 5px;

th.submission-date-col {
width: 8%;
}

@media (max-width: 1000px) {
th.submission-date-col {
display: none;
}
}
td {
border-radius: 20%;

div {
margin-top: 100%;
}

&.activity-label {
position: relative;
white-space: nowrap;
}

&.activity-blank {
background-color: white;
}
&.activity-0 {
background-color: #ddd;
}
&.activity-1 {
background-color: #9be9a8;
}
&.activity-2 {
background-color: #40c463;
}
&.activity-3 {
background-color: #2f9c4c;
}
&.activity-4 {
background-color: #216e39;
}
}
}
}
}
2 changes: 2 additions & 0 deletions resources/vars.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ $announcement_red: #ae0000;
$base_font_size: 14px;
$widget_border_radius: 4px;

$table_header_rounding: 6px;

$monospace-fonts: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;
177 changes: 176 additions & 1 deletion templates/user/user-about.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,69 @@ <h4>{{ _('About') }}</h4>
<br>
{% endif %}

<h4>{{ _('Submission Activity') }}</h4>
<div id="submission-activity" style="display: none;">
<div id="submission-activity-actions">
<a href="javascript:void(0)" id="prev-year-action">&laquo;</a>
&nbsp;<span id="year"></span>&nbsp;
<a href="javascript:void(0)" id="next-year-action">&raquo;</a>
</div>
<div id="submission-activity-display">
<table id="submission-activity-table">
<tr id="submission-0">
<th class="submission-date-col info-text">
{{ _('Mon') }}
</th>
</tr>
<tr id="submission-1">
<th class="submission-date-col info-text">
{{ _('Tues') }}
</th>
</tr>
<tr id="submission-2">
<th class="submission-date-col info-text">
{{ _('Wed') }}
</th>
</tr>
<tr id="submission-3">
<th class="submission-date-col info-text">
{{ _('Thurs') }}
</th>
</tr>
<tr id="submission-4">
<th class="submission-date-col info-text">
{{ _('Fri') }}
</th>
</tr>
<tr id="submission-5">
<th class="submission-date-col info-text">
{{ _('Sat') }}
</th>
</tr>
<tr id="submission-6">
<th class="submission-date-col info-text">
{{ _('Sun') }}
</th>
</tr>
</table>
<div class="info-bar">
<span id="submission-total-count" class="info-text">
</span>
<table class="info-table">
<tr>
<th class="info-table-text info-text">{{ _('Less') }}</th>
<td class="activity-0"><div></div></td>
<td class="activity-1"><div></div></td>
<td class="activity-2"><div></div></td>
<td class="activity-3"><div></div></td>
<td class="activity-4"><div></div></td>
<th class="info-table-text info-text">{{ _('More') }}</th>
<tr>
</table>
</div>
</div>
</div>

{% if rating %}
<h4>Rating History</h4>
<div id="rating-chart">
Expand All @@ -67,10 +130,122 @@ <h4>Rating History</h4>
{% include "mathjax-load.html" %}
{% endif %}

<script type="text/javascript">
var submission_activity = {{ submission_data }};
var metadata = {{ submission_metadata }};
const activity_levels = 5; // 5 levels of activity

$(document).ready(function () {
var active_tooltip = null;

function display_tooltip(where) {
if (active_tooltip !== null) {
active_tooltip.removeClass(['tooltipped', 'tooltipped-e', 'tooltipped-w']).removeAttr('aria-label');
}
if (where !== null) {
var day_num = parseInt(where.attr('data-day'));
var tooltip_direction = day_num < 183 ? 'tooltipped-e' : 'tooltipped-w';
where.addClass(['tooltipped', tooltip_direction])
.attr('aria-label', where.attr('data-submission-activity'));
}
active_tooltip = where;
}

function install_tooltips () {
display_tooltip(null);
$('.activity-label').each(function () {
var link = $(this);
link.hover(
function(e) {
display_tooltip(link);
},
function(e) {
display_tooltip(null);
}
);
});
}

var current_year = new Date().getFullYear();
var $div = $('#submission-activity');

function draw_contribution(year) {
$div.find('#submission-activity-table td').remove();
$div.find('#year').attr('data-year', year);
$div.find('#prev-year-action').css('display', year > metadata.min_year ? '' : 'none');
$div.find('#next-year-action').css('display', year < current_year ? '' : 'none');
var current_weekday = 0;
var start_day = new Date(year, 0, 1)
var end_day = new Date(year + 1, 0, 0);
if (year == current_year) {
end_day = new Date();
start_day = new Date(end_day.getFullYear() - 1, end_day.getMonth(), end_day.getDate() + 1);
$div.find('#year').text("{{ _('past year') }}");
} else {
$div.find('#year').text(year);
}
var days = [];
for (var day = start_day, day_num = 1; day <= end_day; day.setDate(day.getDate() + 1), day_num++) {
var isodate = day.toISOString().split('T')[0];
days.push({
date: new Date(day),
weekday: day.getDay(),
day_num: day_num,
activity: submission_activity[isodate] || 0,
});
}

var sum_activity = days.map(obj => obj.activity).reduce((a, b) => a + b, 0);
// There doesn't seem to be a way to internationalize strings in JavaScript.
$div.find('#submission-total-count').text(
"{{ _('%%(cnt)d total submissions') }}".replace('%(cnt)d', sum_activity)
)

var max_activity = Math.max.apply(null, days.map(obj => obj.activity));
var diff = max_activity / (activity_levels - 1);
var activity_breakdown = [...Array(activity_levels).keys()].map(x => diff * x);

for (; current_weekday < days[0].weekday; current_weekday++) {
$div.find('#submission-' + current_weekday)
.append($('<td></td>').addClass('activity-blank').append('<div></div>'));
}

days.forEach(obj => {
var level = activity_breakdown.findIndex(x => x >= obj.activity);
// There doesn't seem to be a way to internationalize strings in JavaScript.
var text = "{{ _('%%(cnt)d submissions on %%b %%d, %%Y') }}"
.replace('%(cnt)d', obj.activity)
.replace('%b', metadata.month_names[obj.date.getMonth()])
.replace('%d', obj.date.getDate())
.replace('%Y', obj.date.getFullYear());
$div.find('#submission-' + obj.weekday)
.append(
$('<td></td>').addClass(['activity-label', 'activity-' + level])
.attr('data-submission-activity', text)
.attr('data-day', obj.day_num)
.append('<div></div>')
);
});

install_tooltips();
}

$('#prev-year-action').click(function () {
draw_contribution(parseInt($div.find('#year').attr('data-year')) - 1);
});
$('#next-year-action').click(function () {
draw_contribution(parseInt($div.find('#year').attr('data-year')) + 1);
});

draw_contribution(current_year);
$('#submission-activity').css('display', '');
});
</script>

{% if ratings %}
<script type="text/javascript" src="{{ static('libs/chart.js/Chart.js') }}"></script>
<script type="text/javascript">
var rating_history = {{rating_data}};
var rating_history = {{ rating_data }};

$.each(rating_history, function (index, item) {
item.x = new Date(item.timestamp);
Expand Down

0 comments on commit 5248d02

Please sign in to comment.