Merge branch 'graph' into main

This commit is contained in:
ix 2021-09-08 04:00:39 +02:00
commit 8a203161ed
6 changed files with 412 additions and 102 deletions

View File

@ -1,13 +1,20 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% load i18n %} {% load i18n %}
{% load key_value %}
<div class="row"> <div class="row">
<div class="col-sm-12 col-md-9"> <div class="col-sm-12 col-md-9">
<div> <div>
<h4>Challenges Solved by {{ user.username }}</h4> <h4>Challenges Solved by {{ user.username }}</h4>
{% if solves%} {% if solves%}
<table class="table table-dark">
<thead> <div class="table table-dark">
<div class="card-body">
<div id="time-chart"></div>
</div>
</div>
<table class="table table-dark">
<thead>
<tr> <tr>
<th scope="col">{% trans "Challenge Name" %}</th> <th scope="col">{% trans "Challenge Name" %}</th>
<th scope="col">{% trans "Category" %}</th> <th scope="col">{% trans "Category" %}</th>
@ -46,5 +53,271 @@
</ul> </ul>
</div> </div>
</div> </div>
<script src="https://code.highcharts.com/highcharts.src.js"></script>
<script>
Highcharts.theme = {
colors: ['#2b908f', '#90ee7e', '#f45b5b', '#7798BF', '#aaeeee', '#ff0066',
'#eeaaee', '#55BF3B', '#DF5353', '#7798BF', '#aaeeee'],
chart: {
backgroundColor: {
linearGradient: { x1: 0, y1: 0, x2: 1, y2: 1 },
stops: [
[0, '#1D1D1D'],
[1, '#1D1D1D']
]
},
style: {
fontFamily: '\'Unica One\', sans-serif'
},
plotBorderColor: '#606063'
},
title: {
style: {
color: '#E0E0E3',
textTransform: 'uppercase',
fontSize: '20px'
}
},
subtitle: {
style: {
color: '#E0E0E3',
textTransform: 'uppercase'
}
},
xAxis: {
gridLineColor: '#707073',
labels: {
style: {
color: '#E0E0E3'
}
},
lineColor: '#707073',
minorGridLineColor: '#505053',
tickColor: '#707073',
title: {
style: {
color: '#A0A0A3'
}
}
},
yAxis: {
gridLineColor: '#707073',
labels: {
style: {
color: '#E0E0E3'
}
},
lineColor: '#707073',
minorGridLineColor: '#505053',
tickColor: '#707073',
tickWidth: 1,
title: {
style: {
color: '#A0A0A3'
}
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.85)',
style: {
color: '#F0F0F0'
}
},
plotOptions: {
series: {
dataLabels: {
color: '#F0F0F3',
style: {
fontSize: '13px'
}
},
marker: {
lineColor: '#333'
}
},
boxplot: {
fillColor: '#505053'
},
candlestick: {
lineColor: 'white'
},
errorbar: {
color: 'white'
}
},
legend: {
backgroundColor: '#1D1D1D',
itemStyle: {
color: '#E0E0E3'
},
itemHoverStyle: {
color: '#FFF'
},
itemHiddenStyle: {
color: '#606063'
},
title: {
style: {
color: '#C0C0C0'
}
}
},
credits: {
style: {
color: '#666'
}
},
labels: {
style: {
color: '#707073'
}
},
drilldown: {
activeAxisLabelStyle: {
color: '#F0F0F3'
},
activeDataLabelStyle: {
color: '#F0F0F3'
}
},
navigation: {
buttonOptions: {
symbolStroke: '#DDDDDD',
theme: {
fill: '#505053'
}
}
},
// scroll charts
rangeSelector: {
buttonTheme: {
fill: '#505053',
stroke: '#000000',
style: {
color: '#CCC'
},
states: {
hover: {
fill: '#707073',
stroke: '#000000',
style: {
color: 'white'
}
},
select: {
fill: '#000003',
stroke: '#000000',
style: {
color: 'white'
}
}
}
},
inputBoxBorderColor: '#505053',
inputStyle: {
backgroundColor: '#333',
color: 'silver'
},
labelStyle: {
color: 'silver'
}
},
navigator: {
handles: {
backgroundColor: '#666',
borderColor: '#AAA'
},
outlineColor: '#CCC',
maskFill: 'rgba(255,255,255,0.1)',
series: {
color: '#7798BF',
lineColor: '#A6C7ED'
},
xAxis: {
gridLineColor: '#505053'
}
},
scrollbar: {
barBackgroundColor: '#808083',
barBorderColor: '#808083',
buttonArrowColor: '#CCC',
buttonBackgroundColor: '#606063',
buttonBorderColor: '#606063',
rifleColor: '#FFF',
trackBackgroundColor: '#404043',
trackBorderColor: '#404043'
}
};
// Apply the theme
Highcharts.setOptions(Highcharts.theme);
Highcharts.chart('time-chart', {
title: {
text: 'Points earned for each category'
},
yAxis: {
title: {
text: 'Points earned'
}
},
xAxis: {
type: 'datetime',
// Use the date format in the
// labels property of the chart
labels: {
formatter: function() {
return Highcharts.dateFormat('%d.%b %Y',
this.value);
}
}
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'middle'
},
plotOptions: {
pointStart: {{ user.date_joined|timestamp_fromdate }},
series: {
label: {
connectorAllowed: false
},
allowPointSelect: true,
marker: {
enabled: true
}
}
},
series: [
{
name: 'Total',
data: {{ solved|safe }}
},
{% for cat in cats %}
{
name: '{{ cat.name }}',
data: {{ pointDatas|keyvalue:cat.name|safe }},
visible: false,
},
{% endfor %}
],
responsive: {
rules: [{
condition: {
maxWidth: 500
},
chartOptions: {
legend: {
layout: 'horizontal',
align: 'center',
verticalAlign: 'bottom'
}
}
}]
}
});
</script>
{% endblock %} {% endblock %}

View File

View File

@ -0,0 +1,11 @@
from django import template
register = template.Library()
@register.filter
def keyvalue(dict, key):
return dict[key]
@register.filter
def timestamp_fromdate(date):
return str(date.timestamp() * 1000).replace(',','.')

View File

@ -2,7 +2,7 @@ from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django import forms from django import forms
from ctfs.models import CTF_flags from ctfs.models import Category, CTF_flags
from ..forms import UserForm,UserProfileInfoForm, UserInfosUpdateForm, UserUpdateForm from ..forms import UserForm,UserProfileInfoForm, UserInfosUpdateForm, UserUpdateForm
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -18,113 +18,139 @@ from accounts.models import UserProfileInfo
from . import connection from . import connection
def signin(request): def signin(request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
if request.method == 'POST': if request.method == 'POST':
username = request.POST.get('username') username = request.POST.get('username')
password = request.POST.get('password') password = request.POST.get('password')
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
if user: if user:
if user.is_active: if user.is_active:
login(request,user) login(request,user)
return HttpResponseRedirect(reverse('home')) return HttpResponseRedirect(reverse('home'))
else: else:
return HttpResponse(_("Your account was inactive.")) return HttpResponse(_("Your account was inactive."))
else: else:
return render(request, 'accounts/login.html', {'error': True}) return render(request, 'accounts/login.html', {'error': True})
else: else:
return render(request, 'accounts/login.html', {}) return render(request, 'accounts/login.html', {})
else: else:
return HttpResponseRedirect(reverse('home')) return HttpResponseRedirect(reverse('home'))
def signup(request): def signup(request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
user_form = UserForm() user_form = UserForm()
profile_form = UserProfileInfoForm() profile_form = UserProfileInfoForm()
registered = False registered = False
if request.method == 'POST': if request.method == 'POST':
pass1 = request.POST.get('password') pass1 = request.POST.get('password')
if len(pass1) < 8: if len(pass1) < 8:
return render(request,'accounts/register.html', {'user_form':user_form, 'profile_form':profile_form, 'registered_failed':"The new password must be at least %d characters long." % 8}) return render(request,'accounts/register.html', {'user_form':user_form, 'profile_form':profile_form, 'registered_failed':"The new password must be at least %d characters long." % 8})
first_isalpha = pass1[0].isalpha() first_isalpha = pass1[0].isalpha()
if not any(c.isdigit() for c in pass1) or not any(c.isalpha() for c in pass1): if not any(c.isdigit() for c in pass1) or not any(c.isalpha() for c in pass1):
return render(request,'accounts/register.html', {'user_form':user_form, 'profile_form':profile_form, 'registered_failed':_("The password must contain at least one letter and at least one digit or punctuation character.")}) return render(request,'accounts/register.html', {'user_form':user_form, 'profile_form':profile_form, 'registered_failed':_("The password must contain at least one letter and at least one digit or punctuation character.")})
if User.objects.filter(email=request.POST.get('email')).exists(): if User.objects.filter(email=request.POST.get('email')).exists():
return render(request,'accounts/register.html', {'user_form':user_form, 'profile_form':profile_form, 'registered_failed':_("A user with that email already exists.")}) return render(request,'accounts/register.html', {'user_form':user_form, 'profile_form':profile_form, 'registered_failed':_("A user with that email already exists.")})
user_form = UserForm(data=request.POST) user_form = UserForm(data=request.POST)
profile_form = UserProfileInfoForm(data=request.POST) profile_form = UserProfileInfoForm(data=request.POST)
if user_form.is_valid() and profile_form.is_valid(): if user_form.is_valid() and profile_form.is_valid():
user = user_form.save() user = user_form.save()
user.set_password(user.password) user.set_password(user.password)
user.save() user.save()
profile = profile_form.save(commit=False) profile = profile_form.save(commit=False)
profile.user = user profile.user = user
profile.token = token_hex(16) profile.token = token_hex(16)
profile.save() profile.save()
registered = True registered = True
else: else:
return render(request,'accounts/register.html', {'user_form':user_form, 'profile_form':profile_form, 'registered_failed':_("A user with that username already exists.")}) return render(request,'accounts/register.html', {'user_form':user_form, 'profile_form':profile_form, 'registered_failed':_("A user with that username already exists.")})
return render(request,'accounts/register.html', return render(request,'accounts/register.html',
{'user_form':user_form, {'user_form':user_form,
'profile_form':profile_form, 'profile_form':profile_form,
'registered':registered}) 'registered':registered})
else: else:
return HttpResponseRedirect(reverse('home')) return HttpResponseRedirect(reverse('home'))
@login_required @login_required
def out(request): def out(request):
logout(request) logout(request)
return HttpResponseRedirect(reverse('home')) return HttpResponseRedirect(reverse('home'))
@login_required @login_required
def edit(request): def edit(request):
if request.method == 'POST': if request.method == 'POST':
umail = request.user.email umail = request.user.email
uuser = request.user.username uuser = request.user.username
p_form = UserInfosUpdateForm(request.POST, instance=request.user.userprofileinfo) p_form = UserInfosUpdateForm(request.POST, instance=request.user.userprofileinfo)
u_form = UserUpdateForm(request.POST, instance=request.user) u_form = UserUpdateForm(request.POST, instance=request.user)
error = None error = None
success = None success = None
if p_form.is_valid() and u_form.is_valid(): if p_form.is_valid() and u_form.is_valid():
pmail = u_form.cleaned_data['email'] pmail = u_form.cleaned_data['email']
if pmail == umail: if pmail == umail:
pass pass
else: else:
if User.objects.filter(email=pmail).exists(): if User.objects.filter(email=pmail).exists():
error = _("Email already taken.") error = _("Email already taken.")
puser = u_form.cleaned_data['username'] puser = u_form.cleaned_data['username']
if puser == uuser: if puser == uuser:
pass pass
else: else:
if User.objects.filter(username=puser).exists(): if User.objects.filter(username=puser).exists():
error = _("Username already taken.") error = _("Username already taken.")
if error is None: if error is None:
u_form.save() u_form.save()
p_form.save() p_form.save()
success = _("Updated.") success = _("Updated.")
request.user.username = uuser request.user.username = uuser
context={'p_form': p_form, 'u_form': u_form, 'error':error, 'success' : success} context={'p_form': p_form, 'u_form': u_form, 'error':error, 'success' : success}
return render(request, 'accounts/edit.html', context) return render(request, 'accounts/edit.html', context)
else: else:
p_form = UserInfosUpdateForm(instance=request.user.userprofileinfo) p_form = UserInfosUpdateForm(instance=request.user.userprofileinfo)
u_form = UserUpdateForm(instance=request.user) u_form = UserUpdateForm(instance=request.user)
context={'p_form': p_form, 'u_form': u_form, 'token': request.user.userprofileinfo.token} context={'p_form': p_form, 'u_form': u_form, 'token': request.user.userprofileinfo.token}
return render(request, 'accounts/edit.html',context ) return render(request, 'accounts/edit.html',context )
@login_required @login_required
def profile(request, user_name): def profile(request, user_name):
user_obj = get_object_or_404(User, username=user_name) globalLabels= []
solves = CTF_flags.objects.filter(user=user_obj).order_by('-flag_date') globalDatas = []
return render(request,'accounts/profile.html', {'user':user_obj, 'solves':solves})
user_obj = get_object_or_404(User, username=user_name)
cats = Category.objects.all()
pointDatas = {}
for cat in cats:
# prepare categories
globalLabels.append(cat.name)
solved_count = CTF_flags.objects.filter(user=user_obj, ctf__category__name=cat.name).order_by('-flag_date').count()
globalDatas.append(solved_count)
# get datas
somme = 0
solved = CTF_flags.objects.filter(user=user_obj, ctf__category__name=cat.name).order_by('flag_date')
pointDatas[cat.name] = []
pointDatas[cat.name].append([user_obj.date_joined.timestamp() * 1000, 0])
for flag in solved:
somme += flag.ctf.points
pointDatas[cat.name].append([flag.flag_date.timestamp() * 1000, somme])
solves = CTF_flags.objects.filter(user=user_obj).order_by('-flag_date')
solved = []
somme = 0
solved.append([user_obj.date_joined.timestamp() * 1000, 0])
for s in solves.reverse():
somme += s.ctf.points
solved.append([s.flag_date.timestamp() * 1000,somme])
return render(request,'accounts/profile.html', {'user':user_obj, 'solves':solves,'solved':solved,'globalLabels': globalLabels, 'globalDatas': globalDatas, 'pointDatas': pointDatas})
# Create your views here. # Create your views here.
def rank(request, token): def rank(request, token):
all_users = UserProfileInfo.objects.filter(score__gt=0).select_related().order_by('-score', 'last_submission_date', 'user__username') all_users = UserProfileInfo.objects.filter(score__gt=0).select_related().order_by('-score', 'last_submission_date', 'user__username')
rank = 1 rank = 1
for elem in all_users: for elem in all_users:
if elem.token == token: if elem.token == token:
break break
rank += 1 rank += 1
data = {"rank": rank} data = {"rank": rank}
return JsonResponse(data) return JsonResponse(data)

View File

@ -17,7 +17,7 @@ pre {background-color: #000; color: #cecece; padding-left: 15px; font-weight: bo
.dropdown-item:hover {background-color: #1D1D1D; color: #FFFFFF} .dropdown-item:hover {background-color: #1D1D1D; color: #FFFFFF}
.flag_link {margin-right: 6px} .flag_link {margin-right: 6px}
.flag_img {margin-top: 10px;width: 28px;border-radius: 100%;} .flag_img {margin-top: 10px;width: 28px;}
.table-dark {background-color: #1D1D1D} .table-dark {background-color: #1D1D1D}
.table-dark td, .table-dark th, .table-dark thead th {border: none} .table-dark td, .table-dark th, .table-dark thead th {border: none}

View File

@ -17,7 +17,7 @@ pre {background-color: #000; color: #cecece; padding-left: 15px; font-weight: bo
.dropdown-item:hover {background-color: #1D1D1D; color: #FFFFFF} .dropdown-item:hover {background-color: #1D1D1D; color: #FFFFFF}
.flag_link {margin-right: 6px} .flag_link {margin-right: 6px}
.flag_img {margin-top: 10px;width: 28px;border-radius: 100%;} .flag_img {margin-top: 10px;width: 28px;}
.table-dark {background-color: #1D1D1D} .table-dark {background-color: #1D1D1D}
.table-dark td, .table-dark th, .table-dark thead th {border: none} .table-dark td, .table-dark th, .table-dark thead th {border: none}