diff --git a/nginx/nginx.conf b/nginx/nginx.conf index d9f9a23..fca61c6 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -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/; diff --git a/src/core/views.py b/src/core/views.py index 63d0793..04dab8a 100644 --- a/src/core/views.py +++ b/src/core/views.py @@ -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): diff --git a/src/events/forms.py b/src/events/forms.py index f9fa912..3e69cf0 100644 --- a/src/events/forms.py +++ b/src/events/forms.py @@ -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')) \ No newline at end of file diff --git a/src/events/migrations/0004_registration_category_object.py b/src/events/migrations/0004_registration_category_object.py new file mode 100644 index 0000000..4ba1d3f --- /dev/null +++ b/src/events/migrations/0004_registration_category_object.py @@ -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'), + ), + ] diff --git a/src/events/migrations/0005_category_laps_count.py b/src/events/migrations/0005_category_laps_count.py new file mode 100644 index 0000000..1b192f2 --- /dev/null +++ b/src/events/migrations/0005_category_laps_count.py @@ -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'), + ), + ] diff --git a/src/events/models.py b/src/events/models.py index 06eaf24..40a668e 100644 --- a/src/events/models.py +++ b/src/events/models.py @@ -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): diff --git a/src/events/urls.py b/src/events/urls.py index b4330e4..d4494e0 100644 --- a/src/events/urls.py +++ b/src/events/urls.py @@ -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//", - 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//", + RegistrationView.as_view(), + name="registration", + ), + + path( + "registrations//present/", + RegistrationPresentView.as_view(), + name="registration_present", + ), + + path( + "category//results/", + ResultsView.as_view(), + name="results", + ) + ] \ No newline at end of file diff --git a/src/events/views.py b/src/events/views.py index 772e932..bee53fa 100644 --- a/src/events/views.py +++ b/src/events/views.py @@ -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 \ No newline at end of file diff --git a/src/templates/core/index.html b/src/templates/core/index.html index cdf0696..9c5d433 100644 --- a/src/templates/core/index.html +++ b/src/templates/core/index.html @@ -149,31 +149,3 @@ {% endblock %} -{% block js %} - -{% endblock %} \ No newline at end of file diff --git a/src/templates/events/presentation.html b/src/templates/events/presentation.html new file mode 100644 index 0000000..2a1cd00 --- /dev/null +++ b/src/templates/events/presentation.html @@ -0,0 +1,73 @@ +{% extends "layout/landing_page.html" %} + +{% load i18n %} +{% load event_tags %} + +{% block extra_head %} + + + + +{% endblock %} + + +{% block landing_content %} +
+ + + + + + + + + + + + + + + + {% for registration in registrations %} + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Krestni jmenoPrijmeniEmailKategorieVariabilni symbolStartovni cisloKod cipuPrezentovane
{{ registration.first_name }}{{ registration.last_name }}{{ registration.email }}{{ registration.category }}{{ registration.variable_symbol }}{{ registration.start_number }}{{ registration.chip_id }}{{ registration.is_presented }} + Upravit + {% if not registration.is_presented %} + Prezentovat + {% endif %} +
Nemáš žádné registrace na tomto závodu.
+ + +
+ {% include 'layout/landing_footer.html' %} +{% endblock %} + +{% block js %} + +{% endblock %} diff --git a/src/templates/events/presentation_confirm.html b/src/templates/events/presentation_confirm.html new file mode 100644 index 0000000..cde09fd --- /dev/null +++ b/src/templates/events/presentation_confirm.html @@ -0,0 +1,53 @@ +{% extends "layout/landing_page.html" %} + +{% load i18n %} +{% load event_tags %} + + +{% block landing_content %} +
+ +

Prezentovat registraci

+ +

Zavodnik: {{ registration.first_name }} {{ registration.last_name }} ({{ registration.nickname }})

+

Kontakt: {{ registration.email }}, {{ registration.phone }}

+

Kategorie: {{ registration.category }}

+
+

Nezabudni priradit kartu

+ +
+ {% csrf_token %} + {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} + + + + {% for field in form %} + + + + + {% endfor %} +
+ {{ field }} + {% if field.errors %} + {{ field.errors }} + {% endif %} +
+ + + + + +
+ + +
+ {% include 'layout/landing_footer.html' %} +{% endblock %} + + diff --git a/src/templates/events/registrations_list.html b/src/templates/events/registrations_list.html new file mode 100644 index 0000000..55d66f1 --- /dev/null +++ b/src/templates/events/registrations_list.html @@ -0,0 +1,59 @@ +{% extends "layout/landing_page.html" %} + +{% load i18n %} +{% load event_tags %} + +{% block landing_content %} +
+ +
+

Tvoje registrace na {{ event.name }}

+
+ {% for registration in registrations %} +

{{ registration }}

+ {% empty %} +

Nemáš žádné registrace na tomto závodu.

+ {% endfor %} +
+ + +
+ + + + + + +
+ {% include 'layout/landing_footer.html' %} +{% endblock %} + +{% block js %} + +{% endblock %} \ No newline at end of file diff --git a/src/templates/events/results.html b/src/templates/events/results.html new file mode 100644 index 0000000..dabd5c9 --- /dev/null +++ b/src/templates/events/results.html @@ -0,0 +1,84 @@ +{% extends "layout/landing_page.html" %} + +{% load i18n %} +{% load event_tags %} + + +{% block landing_content %} + +
+

Kategorie: {{ category }}

+ +

Dokoncene

+ + + + + + + + + + + + + {% for registration in finished %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
PoradiStartovni cisloJezdecJizdyCelkovej cas
{{ registration.rank }}{{ registration.registration.start_number }}{% if registration.registration.nick_name %}{{ registration.registration.nick_name }}{% else %} + {{ registration.registration.first_name }} + {{ registration.registration.last_name }}{% endif %} + {% for lap in registration.registration.laps.all %} + {{ lap.start|time }} - {{ lap.end|time }} ({{ lap.duration }})
+ {% endfor %} +
{{ registration.duration }}
tady nic neni
+ +

Jeste zavodi

+ + + + + + + + + + {% for registration in unfinished %} + + + + + + {% empty %} + + + + {% endfor %} + +
Startovni cisloJezdecJizdy
{{ registration.start_number }}{% if registration.nick_name %}{{ registration.nick_name }}{% else %} + {{ registration.first_name }} + {{ registration.last_name }}{% endif %} + {% for lap in registration.laps.all %} + {{ lap.start|time }} - {{ lap.end|time }} ({{ lap.duration }})
+ {% endfor %} +
tady nic neni
+ + + +
+{% endblock %} + + + diff --git a/src/templates/layout/landing_page.html b/src/templates/layout/landing_page.html index a4e6122..ae2b6f4 100644 --- a/src/templates/layout/landing_page.html +++ b/src/templates/layout/landing_page.html @@ -9,3 +9,26 @@ {% endblock %} +{% block js %} + +{% endblock %} diff --git a/src/templates/menu/landing_page_menu.html b/src/templates/menu/landing_page_menu.html index 997589d..018123b 100644 --- a/src/templates/menu/landing_page_menu.html +++ b/src/templates/menu/landing_page_menu.html @@ -9,12 +9,38 @@ // DOMOV {% for section in html_sections %} - {{ section.menu }} + {{ section.menu }} {% endfor %} // TITULY a KATEGORIE // REGISTRACE // KONTAKT + + + {% if user.is_authenticated %} + +
+ +
+ {% for c in event.categories.all %} + {{ c }} + {% endfor %} +
+
+ + + {% if user.is_superuser %} +
+ + +
+ + {% endif %} + LOGOUT {% endif %} @@ -22,12 +48,28 @@ diff --git a/src/timer/views.py b/src/timer/views.py index 16d237b..ea4ab69 100644 --- a/src/timer/views.py +++ b/src/timer/views.py @@ -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) \ No newline at end of file + return JsonResponse({"status": "ok"}, status=http.HTTPStatus.OK, safe=False) diff --git a/src/users/migrations/0003_alter_user_managers.py b/src/users/migrations/0003_alter_user_managers.py new file mode 100644 index 0000000..530ed50 --- /dev/null +++ b/src/users/migrations/0003_alter_user_managers.py @@ -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=[ + ], + ), + ]