pridane vyuhgodnocovanie

This commit is contained in:
2025-03-23 19:15:54 +01:00
parent c839638ba7
commit 3e8031da1d
17 changed files with 592 additions and 59 deletions

View File

@@ -10,6 +10,7 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
client_max_body_size 50M;
}
location /static/ {
alias /code/static/;

View File

@@ -7,21 +7,27 @@ from django.views.generic import TemplateView
from events.models import Event
class IndexView(TemplateView):
class DefaultContextMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['event'] = Event.objects.get_actual()
if context['event']:
context['html_sections'] = context['event'].html_sections.all().order_by('order_index')
return context
def get(self, request, *args, **kwargs):
data = self.get_context_data()
def dispatch(self, request, *args, **kwargs):
try:
data['event'] = Event.objects.get_actual()
if not data['event']:
event = Event.objects.get_actual()
if not event:
messages.error(request, _('No data found.'))
return HttpResponseRedirect(reverse_lazy('no_data'))
data['html_sections'] = data['event'].html_sections.all().order_by('order_index')
except Exception as e:
HttpResponseRedirect(reverse_lazy('no_data'))
messages.error(request, _('No data found.'))
return HttpResponseRedirect(reverse_lazy('no_data'))
return super().dispatch(request, *args, **kwargs)
return self.render_to_response(data)
class IndexView(DefaultContextMixin, TemplateView):
template_name = 'core/index.html'
class NoDataView(TemplateView):

View File

@@ -3,6 +3,8 @@ from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, HTML, Layout, Fieldset, Submit, Field
from events.models import Registration
class RegistrationForm(forms.Form):
email = forms.CharField(label=_('Email'), required=True)
@@ -31,4 +33,48 @@ class EmailForm(forms.Form):
self.helper.form_tag = False
self.helper.form_method = 'post'
self.helper.form_action = 'submit_survey'
self.helper.add_input(Submit('submit', 'Submit', css_class='btn btn-primary d-block mx-auto'))
class UpdateRegistrationForm(forms.ModelForm):
class Meta:
model = Registration
fields = ['first_name', 'last_name', 'nick_name', 'phone', 'category', 'is_confirmed', 'is_paid', 'is_finished', 'is_presented', 'start_number', 'chip_id']
labels = {
'first_name': _('Krsteni jmeno'),
'last_name': _('Prijmeni'),
'nick_name': _('Prezdivka'),
'phone': _('Telefon'),
'category': _('Kateogrie'),
'is_paid': _('Uhradena'),
'is_finished': _('Dokončena'),
'is_presented': _('Prezentovana'),
'start_number': _('Startovni cislo'),
'chip_id': _('Kod cipu'),
}
def __init__(self, *args, **kwargs):
super(UpdateRegistrationForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = 'post'
self.helper.form_action = 'submit_survey'
self.helper.add_input(Submit('submit', 'Submit', css_class='btn btn-primary d-block mx-auto'))
class PresentRegistrationForm(forms.ModelForm):
class Meta:
model = Registration
fields = ['start_number', 'chip_id', 'category_object']
labels = {
'start_number': _('Startovni cislo'),
'chip_id': _('Kod cipu'),
'category_object': _('Kategorie'),
}
def __init__(self, *args, **kwargs):
super(PresentRegistrationForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = 'post'
self.helper.form_action = 'submit_survey'
self.helper.add_input(Submit('submit', 'Submit', css_class='btn btn-primary d-block mx-auto'))

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.6 on 2025-03-22 21:10
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0003_alter_registration_event'),
]
operations = [
migrations.AddField(
model_name='registration',
name='category_object',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='registrations', to='events.category', verbose_name='Category'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-22 21:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0004_registration_category_object'),
]
operations = [
migrations.AddField(
model_name='category',
name='laps_count',
field=models.IntegerField(blank=True, default=1, null=True, verbose_name='Laps count'),
),
]

View File

@@ -108,6 +108,8 @@ class Category(models.Model):
climb = models.IntegerField(blank=True, null=True, verbose_name=_('Climb'))
gpx = models.FileField(upload_to='gpx/', blank=True, null=True, verbose_name=_('GPX'))
laps_count = models.IntegerField(blank=True, null=True, default=1, verbose_name=_('Laps count'))
class Meta:
verbose_name = _('Category')
verbose_name_plural = _('Categories')
@@ -140,13 +142,15 @@ class Registration(TimeStampedModel):
# race data
start_number = models.IntegerField(blank=True, null=True, verbose_name=_('Start number'))
chip_id = models.CharField(max_length=30, blank=True, null=True, verbose_name=_('Chip ID'))
category_object = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL,
related_name='registrations', verbose_name=_('Category'))
class Meta:
verbose_name = _('Registration')
verbose_name_plural = _('Registrations')
def __str__(self):
return f'{self.email} - {self.event}'
return f'{self.get_name()} - {self.event}'
def save(self, *args, **kwargs):
if not self.uid:
@@ -174,6 +178,24 @@ class Registration(TimeStampedModel):
else:
return 'Nezaplacena'
@property
def is_ended(self):
"""
Returns whether the racer has finished all the laps.
:return:
"""
return self.laps.filter(end__isnull=False).count() == self.category_object.laps_count
@property
def total_duration(self):
if not self.is_ended:
return timedelta(0)
duration = timedelta(0)
for lap in self.laps.all():
duration += lap.duration
return duration
class Lap(TimeStampedModel):

View File

@@ -1,14 +1,10 @@
from django.urls import path
from events.views import (RegistrationView, ConfirmRegistrationView,
MyRegistrationsRequestView, MyRegistrationsListView)
from events.views import (RegistrationView, ConfirmRegistrationView, RegistrationsListViews, RegistrationPresentView,
MyRegistrationsRequestView, MyRegistrationsListView, ResultsView)
urlpatterns = [
path(
"registration/<int:pk>/",
RegistrationView.as_view(),
name="registration",
),
path(
"confirm-registration/",
@@ -29,4 +25,30 @@ urlpatterns = [
),
path(
"registrations/presentation",
RegistrationsListViews.as_view(
template_name="events/presentation.html"
),
name="registrations_presentation",
),
path(
"registrations/<int:pk>/",
RegistrationView.as_view(),
name="registration",
),
path(
"registrations/<int:pk>/present/",
RegistrationPresentView.as_view(),
name="registration_present",
),
path(
"category/<int:pk>/results/",
ResultsView.as_view(),
name="results",
)
]

View File

@@ -1,4 +1,7 @@
import logging
from datetime import timedelta
from unicodedata import category
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponseRedirect
@@ -10,9 +13,10 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from core.mailer import Emailer
from core.utils import generate_random_string
from events.forms import RegistrationForm, EmailForm
from events.models import Registration, Event
from events.forms import RegistrationForm, EmailForm, PresentRegistrationForm
from events.models import Registration, Event, Category
from users.models import ConfirmationCode, SecurityCode
from core.views import DefaultContextMixin
logger = logging.getLogger(__name__)
@@ -27,10 +31,16 @@ class RegistrationView(RedirectView):
# handle registration form
form = RegistrationForm(request.POST)
if form.is_valid():
try:
if event.registrations_require_confirmation:
# create email security code for feature requests
security_code, created = SecurityCode.objects.get_or_create(email=form.cleaned_data['email'])
try:
reg_category = Category.objects.get(name=form.cleaned_data['category'])
except Category.DoesNotExist:
reg_category = None
try:
# create a new Registration
registration = Registration(
event=event,
@@ -41,6 +51,7 @@ class RegistrationView(RedirectView):
email=form.cleaned_data['email'],
text=form.cleaned_data['text'],
category=form.cleaned_data['category'],
category_object=reg_category,
)
registration.save()
@@ -213,16 +224,12 @@ class MyRegistrationsRequestView(RedirectView):
return HttpResponseRedirect(self.success_url)
class MyRegistrationsListView(TemplateView):
class MyRegistrationsListView(DefaultContextMixin, TemplateView):
template_name = 'events/my_registrations.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# get event by pk
context['event'] = get_object_or_404(Event, uid=kwargs['uid'])
context['html_sections'] = context['event'].html_sections.all().order_by('order_index')
# check security code if user is not admin
if not self.request.user.is_superuser:
security_code = get_object_or_404(SecurityCode, email=kwargs['email'])
@@ -233,3 +240,71 @@ class MyRegistrationsListView(TemplateView):
context['registrations'] = Registration.objects.filter(email=kwargs['email'])
return context
class RegistrationsListViews(LoginRequiredMixin, DefaultContextMixin, TemplateView):
template_name = 'events/registrations_list.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['registrations'] = context['event'].registrations.all().order_by('category')
return context
class RegistrationPresentView(LoginRequiredMixin, DefaultContextMixin, TemplateView):
template_name = 'events/presentation_confirm.html'
success_url = reverse_lazy('registrations_presentation')
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
context['registration'] = get_object_or_404(Registration, pk=kwargs['pk'])
context['form'] = PresentRegistrationForm(instance=context['registration'])
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
registration = get_object_or_404(Registration, pk=kwargs['pk'])
form = PresentRegistrationForm(request.POST, instance=registration)
if form.is_valid():
instance = form.save(commit=False)
instance.is_presented = True
instance.save()
messages.success(request, _('Prezentovano'))
return HttpResponseRedirect(self.success_url)
else:
messages.error(request, _('Please correct the error below.'))
return self.get(request, *args, **kwargs)
class ResultsView(DefaultContextMixin, TemplateView):
template_name = 'events/results.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = get_object_or_404(Category, pk=kwargs['pk'])
context['finished'] = []
context['unfinished'] = []
for registration in context['category'].registrations.all():
if registration.is_ended:
context['finished'].append({
'registration': registration,
'duration': registration.total_duration,
})
else:
context['unfinished'].append(registration)
# sort by duration
context['finished'] = sorted(context['finished'], key=lambda x: x['duration'])
# add rank
rank = 0
last_duration = timedelta(0)
for registration in context['finished']:
if registration['duration'] != last_duration:
rank += 1
registration['rank'] = rank
return context

View File

@@ -149,31 +149,3 @@
{% endblock %}
{% block js %}
<script>
// Change style of navbar on scroll
window.onscroll = function () {
myFunction()
};
function myFunction() {
let navbar = document.getElementById("myNavbar");
if (document.body.scrollTop > 100 || document.documentElement.scrollTop > 100) {
navbar.className = "w3-bar" + " w3-card" + " w3-animate-top" + " w3-white";
} else {
navbar.className = navbar.className.replace(" w3-card w3-animate-top w3-white", "");
}
}
// Used to toggle the menu on small screens when clicking on the menu button
function toggleFunction() {
let x = document.getElementById("navDemo");
if (x.className.indexOf("w3-show") === -1) {
x.className += " w3-show";
} else {
x.className = x.className.replace(" w3-show", "");
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,73 @@
{% extends "layout/landing_page.html" %}
{% load i18n %}
{% load event_tags %}
{% block extra_head %}
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/2.2.2/css/dataTables.dataTables.css" />
<script src="https://cdn.datatables.net/2.2.2/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/plug-ins/2.2.2/i18n/cs.js"></script>
{% endblock %}
{% block landing_content %}
<div class="w3-padding w3-padding-top-64">
<table class="w3-table w3-striped w3-bordered w3-border w3-hoverable w3-white">
<thead>
<tr>
<th>Krestni jmeno</th>
<th>Prijmeni</th>
<th>Email</th>
<th>Kategorie</th>
<th>Variabilni symbol</th>
<th>Startovni cislo</th>
<th>Kod cipu</th>
<th>Prezentovane</th>
<th></th>
</tr>
</thead>
<tbody>
{% for registration in registrations %}
<tr>
<td>{{ registration.first_name }}</td>
<td>{{ registration.last_name }}</td>
<td>{{ registration.email }}</td>
<td>{{ registration.category }}</td>
<td>{{ registration.variable_symbol }}</td>
<td>{{ registration.start_number }}</td>
<td>{{ registration.chip_id }}</td>
<td>{{ registration.is_presented }}</td>
<td>
<a href="{% url 'admin:events_registration_change' registration.id %}" class="w3-button w3-green w3-small" target="_blank">Upravit</a>
{% if not registration.is_presented %}
<a href="{% url 'registration_present' registration.id %}" class="w3-button w3-blue-gray w3-small">Prezentovat</a>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="2">Nemáš žádné registrace na tomto závodu.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include 'layout/landing_footer.html' %}
{% endblock %}
{% block js %}
<script>
$(document).ready(function () {
$('.w3-table').DataTable(
{
"language": {
url: 'https://cdn.datatables.net/plug-ins/2.2.2/i18n/cs.json'
}
}
);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends "layout/landing_page.html" %}
{% load i18n %}
{% load event_tags %}
{% block landing_content %}
<div class="w3-padding w3-padding-top-64">
<h4>Prezentovat registraci</h4>
<p>Zavodnik: {{ registration.first_name }} {{ registration.last_name }} ({{ registration.nickname }})</p>
<p>Kontakt: {{ registration.email }}, {{ registration.phone }}</p>
<p>Kategorie: {{ registration.category }}</p>
<hr>
<p class="w3-text-amber">Nezabudni priradit kartu</p>
<form method="post">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="w3-panel w3-red">
{{ form.non_field_errors }}
</div>
{% endif %}
<table class="w3-table w3-bordered">
{% for field in form %}
<tr>
<td class="w3-third"><label class="w3-small"
for="{{ field.id_for_label }}">{{ field.label }}</label></td>
<td>
{{ field }}
{% if field.errors %}
<span class="w3-text-red w3-small">{{ field.errors }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<button class="w3-button w3-blue-gray w3-section" type="submit">Potvrdit</button>
</form>
</div>
{% include 'layout/landing_footer.html' %}
{% endblock %}

View File

@@ -0,0 +1,59 @@
{% extends "layout/landing_page.html" %}
{% load i18n %}
{% load event_tags %}
{% block landing_content %}
<div class="{{ event.get_bg_color }} w3-padding-24"></div>
<div class="w3-content w3-container w3-padding-64" id="home">
<h3 class="w3-center">Tvoje registrace na {{ event.name }}</h3>
<div class="w3-row">
{% for registration in registrations %}
<p>{{ registration }}</p>
{% empty %}
<p class="w3-center">Nemáš žádné registrace na tomto závodu.</p>
{% endfor %}
</div>
</div>
<hr>
{% include 'layout/landing_footer.html' %}
{% endblock %}
{% block js %}
<script>
// Change style of navbar on scroll
window.onscroll = function () {
myFunction()
};
function myFunction() {
let navbar = document.getElementById("myNavbar");
if (document.body.scrollTop > 100 || document.documentElement.scrollTop > 100) {
navbar.className = "w3-bar" + " w3-card" + " w3-animate-top" + " w3-white";
} else {
navbar.className = navbar.className.replace(" w3-card w3-animate-top w3-white", "");
}
}
// Used to toggle the menu on small screens when clicking on the menu button
function toggleFunction() {
let x = document.getElementById("navDemo");
if (x.className.indexOf("w3-show") === -1) {
x.className += " w3-show";
} else {
x.className = x.className.replace(" w3-show", "");
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,84 @@
{% extends "layout/landing_page.html" %}
{% load i18n %}
{% load event_tags %}
{% block landing_content %}
<div class="w3-padding w3-padding-top-64">
<h2>Kategorie: {{ category }}</h2>
<h3>Dokoncene</h3>
<table class="w3-table w3-striped w3-bordered w3-border w3-hoverable w3-white">
<thead>
<tr>
<th>Poradi</th>
<th>Startovni cislo</th>
<th>Jezdec</th>
<th>Jizdy</th>
<th>Celkovej cas</th>
</tr>
</thead>
<tbody>
{% for registration in finished %}
<tr>
<td>{{ registration.rank }}</td>
<td>{{ registration.registration.start_number }}</td>
<td>{% if registration.registration.nick_name %}{{ registration.registration.nick_name }}{% else %}
{{ registration.registration.first_name }}
{{ registration.registration.last_name }}{% endif %}</td>
<td>
{% for lap in registration.registration.laps.all %}
{{ lap.start|time }} - {{ lap.end|time }} ({{ lap.duration }})<br>
{% endfor %}
</td>
<td>{{ registration.duration }}</td>
</tr>
{% empty %}
<tr>
<td colspan="2">tady nic neni</td>
</tr>
{% endfor %}
</tbody>
</table>
<h3>Jeste zavodi</h3>
<table class="w3-table w3-striped w3-bordered w3-border w3-hoverable w3-white">
<thead>
<tr>
<th>Startovni cislo</th>
<th>Jezdec</th>
<th>Jizdy</th>
</tr>
</thead>
<tbody>
{% for registration in unfinished %}
<tr>
<td>{{ registration.start_number }}</td>
<td>{% if registration.nick_name %}{{ registration.nick_name }}{% else %}
{{ registration.first_name }}
{{ registration.last_name }}{% endif %}</td>
<td>
{% for lap in registration.laps.all %}
{{ lap.start|time }} - {{ lap.end|time }} ({{ lap.duration }})<br>
{% endfor %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="2">tady nic neni</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -9,3 +9,26 @@
{% endblock %}
{% block js %}
<script>
// Used to toggle the menu on small screens when clicking on the menu button
function toggleFunction() {
let x = document.getElementById("navDemo");
if (x.className.indexOf("w3-show") === -1) {
x.className += " w3-show";
} else {
x.className = x.className.replace(" w3-show", "");
}
}
function toggleMobileDropdown() {
var x = document.getElementById("mobileDemoDropdown");
if (x.className.indexOf("w3-show") == -1) {
x.className += " w3-show";
} else {
x.className = x.className.replace(" w3-show", "");
}
}
</script>
{% endblock %}

View File

@@ -9,12 +9,38 @@
</a>
<a href="{% url 'index' %}#home" class="w3-bar-item w3-button">// DOMOV</a>
{% for section in html_sections %}
<a href="{% url 'index' %}#{{ section.identifier }}" class="w3-bar-item w3-button w3-hide-small">{{ section.menu }}</a>
<a href="{% url 'index' %}#{{ section.identifier }}"
class="w3-bar-item w3-button w3-hide-small">{{ section.menu }}</a>
{% endfor %}
<a href="{% url 'index' %}#categories" class="w3-bar-item w3-button w3-hide-small">// TITULY a KATEGORIE</a>
<a href="{% url 'index' %}#registration" class="w3-bar-item w3-button w3-hide-small">// REGISTRACE</a>
<a href="{% url 'index' %}#contact" class="w3-bar-item w3-button w3-hide-small">// KONTAKT</a>
{% if user.is_authenticated %}
<div class="w3-dropdown-hover w3-hide-small">
<button class="w3-button">VYSLEDKY <i class="fa fa-caret-down"></i></button>
<div class="w3-dropdown-content w3-bar-block w3-card-4">
{% for c in event.categories.all %}
<a href="{% url 'results' c.pk %}" class="w3-bar-item w3-button">{{ c }}</a>
{% endfor %}
</div>
</div>
{% if user.is_superuser %}
<div class="w3-dropdown-hover w3-hide-small">
<button class="w3-button">ADMIN <i class="fa fa-caret-down"></i></button>
<div class="w3-dropdown-content w3-bar-block w3-card-4">
<a href="{% url 'registrations_presentation' %}" class="w3-bar-item w3-button">Prezentace</a>
<a href="{% url 'admin:index' %}" class="w3-bar-item w3-button">Admin</a>
</div>
</div>
{% endif %}
<a href="{% url 'logout' %}" class="w3-bar-item w3-button w3-hide-small">LOGOUT</a>
{% endif %}
</div>
@@ -22,12 +48,28 @@
<!-- Navbar on small screens -->
<div id="navDemo" class="w3-bar-block w3-white w3-hide w3-hide-large w3-hide-medium ">
{% for section in html_sections %}
<a href="{% url 'index' %}#{{ section.identifier }}" class="w3-bar-item w3-button" onclick="toggleFunction()">{{ section.menu }}</a>
<a href="{% url 'index' %}#{{ section.identifier }}" class="w3-bar-item w3-button"
onclick="toggleFunction()">{{ section.menu }}</a>
{% endfor %}
<a href="{% url 'index' %}#categories" class="w3-bar-item w3-button" onclick="toggleFunction()">// TITULY a KATEGORIE</a>
<a href="{% url 'index' %}#registration" class="w3-bar-item w3-button" onclick="toggleFunction()">// REGISTRACE</a>
<a href="{% url 'index' %}#categories" class="w3-bar-item w3-button" onclick="toggleFunction()">// TITULY a
KATEGORIE</a>
<a href="{% url 'index' %}#registration" class="w3-bar-item w3-button" onclick="toggleFunction()">//
REGISTRACE</a>
<a href="{% url 'index' %}#contact" class="w3-bar-item w3-button" onclick="toggleFunction()">// KONTAKT</a>
{% if user.is_authenticated %}
{% if user.is_superuser %}
<div class="w3-dropdown-click" id="mobileDropdown">
<button onclick="toggleMobileDropdown()" class="w3-button">ADMIN <i class="fa fa-caret-down"></i>
</button>
<div id="mobileDemoDropdown" class="w3-dropdown-content w3-bar-block w3-card-4 w3-animate-opacity">
<a href="{% url 'registrations_presentation' %}" class="w3-bar-item w3-button w3-orange"
onclick="toggleFunction()">Presentace</a>
<a href="{% url 'admin:index' %}" class="w3-bar-item w3-button w3-red"
onclick="toggleFunction()">Admin</a>
</div>
</div>
{% endif %}
<a href="{% url 'logout' %}" class="w3-bar-item w3-button">LOGOUT</a>
{% endif %}
</div>

View File

@@ -80,4 +80,4 @@ class WriteTimeApiView(View):
return JsonResponse({"status": "ok"}, status=http.HTTPStatus.OK ,safe=False)
except Exception as e:
logger.error(f"Error while writing lap: {e}")
return JsonResponse({"status": "ok"}, status=http.HTTPStatus.OK, safe=False)
return JsonResponse({"status": "ok"}, status=http.HTTPStatus.OK, safe=False)

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-22 21:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0002_add_superuser'),
]
operations = [
migrations.AlterModelManagers(
name='user',
managers=[
],
),
]