1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-21 21:58:25 +02:00

Upload notes from a CSV sheet

This commit is contained in:
2022-05-15 12:23:17 +02:00
parent 5f2cd16071
commit d18f76cf80
7 changed files with 311 additions and 132 deletions

View File

@ -1,11 +1,16 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import csv
import re
from io import StringIO
from typing import Iterable
from bootstrap_datepicker_plus.widgets import DatePickerInput, DateTimePickerInput
from django import forms
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.utils import formats
from django.utils.translation import gettext_lazy as _
from PyPDF3 import PdfFileReader
@ -190,6 +195,69 @@ class PoolTeamsForm(forms.ModelForm):
}
class UploadNotesForm(forms.Form):
file = forms.FileField(
label=_("CSV file:"),
validators=[FileExtensionValidator(allowed_extensions=["csv"])],
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['file'].widget.attrs['accept'] = 'text/csv'
def clean(self):
cleaned_data = super().clean()
if 'file' in cleaned_data:
file = cleaned_data['file']
with file:
try:
csvfile = csv.reader(StringIO(file.read().decode()))
except UnicodeDecodeError:
self.add_error('file', _("This file contains non-UTF-8 content. "
"Please send your sheet as a CSV file."))
self.process(csvfile, cleaned_data)
return cleaned_data
def process(self, csvfile: Iterable[str], cleaned_data: dict):
parsed_notes = {}
for line in csvfile:
line = [s for s in line if s]
if len(line) < 19:
continue
name = line[0]
notes = line[1:19]
if not all(s.isnumeric() for s in notes):
continue
notes = list(map(int, notes))
if max(notes) < 3 or min(notes) < 0:
continue
max_notes = 3 * [20, 16, 9, 10, 9, 10]
for n, max_n in zip(notes, max_notes):
if n > max_n:
self.add_error('file',
_("The following note is higher of the maximum expected value:")
+ str(n) + " > " + str(max_n))
first_name, last_name = tuple(name.split(' ', 1))
jury = User.objects.filter(first_name=first_name, last_name=last_name)
if jury.count() != 1:
self.form.add_error('file', _("The following user was not found:") + " " + name)
continue
jury = jury.get()
vr = jury.registration
parsed_notes[vr] = notes
cleaned_data['parsed_notes'] = parsed_notes
return cleaned_data
class PassageForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()

View File

@ -654,6 +654,15 @@ class Note(models.Model):
default=0,
)
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
reporter_writing: int, reporter_oral: int):
self.defender_writing = defender_writing
self.defender_oral = defender_oral
self.opponent_writing = opponent_writing
self.opponent_oral = opponent_oral
self.reporter_writing = reporter_writing
self.reporter_oral = reporter_oral
def get_absolute_url(self):
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))

View File

@ -54,6 +54,7 @@
<button class="btn btn-success" data-toggle="modal" data-target="#addPassageModal">{% trans "Add passage" %}</button>
<button class="btn btn-primary" data-toggle="modal" data-target="#updatePoolModal">{% trans "Update" %}</button>
<button class="btn btn-primary" data-toggle="modal" data-target="#updateTeamsModal">{% trans "Update teams" %}</button>
<button class="btn btn-primary" data-toggle="modal" data-target="#uploadNotesModal">{% trans "Upload notes from a CSV file" %}</button>
</div>
{% endif %}
</div>
@ -78,6 +79,11 @@
{% trans "Update" as modal_button %}
{% url "participation:pool_update_teams" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="updateTeams" %}
{% trans "Upload notes" as modal_title %}
{% trans "Upload" as modal_button %}
{% url "participation:pool_upload_notes" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadNotes" modal_button_type="success" modal_enctype="multipart/form-data" %}
{% endblock %}
{% block extrajavascript %}
@ -100,6 +106,12 @@
if (!modalBody.html().trim())
modalBody.load("{% url "participation:passage_create" pk=pool.pk %} #form-content")
});
$('button[data-target="#uploadNotesModal"]').click(function() {
let modalBody = $("#uploadNotesModal div.modal-body");
if (!modalBody.html().trim())
modalBody.load("{% url "participation:pool_upload_notes" pk=pool.pk %} #form-content")
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock %}

View File

@ -6,9 +6,10 @@ from django.views.generic import TemplateView
from .views import CreateTeamView, JoinTeamView, MyParticipationDetailView, MyTeamDetailView, NoteUpdateView, \
ParticipationDetailView, PassageCreateView, PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, \
PoolUpdateTeamsView, PoolUpdateView, SolutionUploadView, SynthesisUploadView, TeamAuthorizationsView, \
TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, TeamUploadMotivationLetterView, TournamentCreateView, \
TournamentDetailView, TournamentExportCSVView, TournamentListView, TournamentUpdateView
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, SolutionUploadView, SynthesisUploadView,\
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentListView, TournamentUpdateView
app_name = "participation"
@ -36,6 +37,7 @@ urlpatterns = [
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"),
path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"),
path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),

View File

@ -6,6 +6,7 @@ import os
from zipfile import ZipFile
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied
@ -18,6 +19,7 @@ from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormMixin, ProcessFormView
from django_tables2 import SingleTableView
from magic import Magic
@ -28,7 +30,7 @@ from tfjm.views import AdminMixin, VolunteerMixin
from .forms import JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, PoolForm, \
PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
ValidateParticipationForm
UploadNotesForm, ValidateParticipationForm
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
@ -703,6 +705,43 @@ class PoolUpdateTeamsView(VolunteerMixin, UpdateView):
return self.handle_no_permission()
class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
model = Pool
form_class = UploadNotesForm
template_name = 'participation/upload_notes.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and (self.object.tournament in request.user.registration.organized_tournaments.all()
or request.user.registration in self.object.juries.all()):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
@transaction.atomic
def form_valid(self, form):
pool = self.get_object()
parsed_notes = form.cleaned_data['parsed_notes']
for vr, notes in parsed_notes.items():
if vr not in pool.juries.all():
form.add_error('file', _("The following user is not registered as a jury:") + " " + str(vr))
for i, passage in enumerate(pool.passages.all()):
note = Note.objects.get(jury=vr, passage=passage)
passage_notes = notes[6 * i:6 * (i + 1)]
note.set_all(*passage_notes)
note.save()
messages.success(self.request, _("Notes were successfully uploaded."))
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('participation:pool_detail', args=(self.kwargs['pk'],))
class PassageCreateView(VolunteerMixin, CreateView):
model = Passage
form_class = PassageForm