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:
@ -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()
|
||||
|
@ -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,))
|
||||
|
||||
|
@ -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 %}
|
||||
|
14
apps/participation/templates/participation/upload_notes.html
Normal file
14
apps/participation/templates/participation/upload_notes.html
Normal 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 %}
|
@ -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"),
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user