Skip to content

Commit

Permalink
Add submission activity graph to users page; #236
Browse files Browse the repository at this point in the history
  • Loading branch information
Ninjaclasher committed Oct 17, 2020
1 parent 8631b92 commit 7a4be13
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 3 deletions.
19 changes: 19 additions & 0 deletions judge/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,23 @@ 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({
'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;
188 changes: 187 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 id="submission-activity-header"></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,133 @@ <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

$(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);
$div.find('#submission-total-count').text(
ngettext("%(cnt)d total submission", "%(cnt)d total submissions", sum_activity)
.replace("%(cnt)d", sum_activity)
)
if (year == current_year) {
$('#submission-activity-header').text(
ngettext("%(cnt)d submission in the last year", "%(cnt)d submissions in the last year", sum_activity)
.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>').addClass('activity-blank').append('<div>'));
}

days.forEach(obj => {
var level = activity_breakdown.findIndex(x => x >= obj.activity);
var text =
ngettext("%(cnt)d submission on %(date)s", "%(cnt)d submissions on %(date)s", obj.activity)
.replace('%(cnt)d', obj.activity)
.replace(
'%(date)s',
obj.date.toLocaleDateString(
"{{ LANGUAGE_CODE }}",
{month: 'short', year: 'numeric', day: 'numeric'}
)
)

$div.find('#submission-' + obj.weekday)
.append(
$('<td>').addClass(['activity-label', 'activity-' + level])
.attr('data-submission-activity', text)
.attr('data-day', obj.day_num)
.append('<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 7a4be13

Please sign in to comment.