From 1a84c26f943a80d6867785fbe079f0d7b5119dbe Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 14 Jul 2017 11:05:35 +0200 Subject: [PATCH] Add a view to import HyperPlanning csv files --- common/urls.py | 3 +- requirements.txt | 2 +- stages/forms.py | 5 +- stages/test_files/HYPERPLANNING.csv | 31 ++++++ stages/tests.py | 17 +++- stages/views.py | 97 +++++++++++++++++-- templates/admin/index.html | 3 +- .../{student_import.html => file_import.html} | 2 +- 8 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 stages/test_files/HYPERPLANNING.csv rename templates/{student_import.html => file_import.html} (91%) diff --git a/common/urls.py b/common/urls.py index 8d6f6f3..932e172 100644 --- a/common/urls.py +++ b/common/urls.py @@ -8,7 +8,8 @@ urlpatterns = [ url(r'^$', RedirectView.as_view(url='/admin/', permanent=True), name='home'), url(r'^admin/', admin.site.urls), - url(r'^data-import/', views.StudentImportView.as_view(), name='tabimport'), + url(r'^import_students/', views.StudentImportView.as_view(), name='import-students'), + url(r'^import_hp/', views.HPImportView.as_view(), name='import-hp'), url(r'^attribution/$', views.AttributionView.as_view(), name='attribution'), url(r'^stages/export/(?Pall)?/?$', views.stages_export, name='stages_export'), diff --git a/requirements.txt b/requirements.txt index c9da223..4c46249 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Django==1.11.2 -tabimport +tabimport>=0.4.0 openpyxl==2.2.6 xlrd diff --git a/stages/forms.py b/stages/forms.py index 96656b7..2fd2a0f 100644 --- a/stages/forms.py +++ b/stages/forms.py @@ -30,4 +30,7 @@ class PeriodForm(forms.Form): def __init__(self, data, *args, **kwargs): pass - + + +class UploadHPFileForm(forms.Form): + upload = forms.FileField(label='Fichier HyperPlanning') diff --git a/stages/test_files/HYPERPLANNING.csv b/stages/test_files/HYPERPLANNING.csv new file mode 100644 index 0000000..6edcc34 --- /dev/null +++ b/stages/test_files/HYPERPLANNING.csv @@ -0,0 +1,31 @@ +NOMPERSO_ENS;UID_ENS;LIBELLE_MAT;NOMPERSO_DIP;TOTAL +Bernasconi Laurie;;#ASE Colloque;#Mandat_ASE;24.00 +Bernasconi Laurie;;#ASE MP Expérientiel;#Mandat_ASE;40.00 +Bernasconi Laurie;;#ASE TA;#Mandat_ASE;18.00 +Bernasconi Laurie;;#IFFP;; +Bernasconi Laurie;;#IFFP;; +Bernasconi Laurie;;#IFFP;#Mandat_ASE;172.00 +Bernasconi Laurie;;Animer I (B);1ASEFEa, 1ASEFEd, 1ASEFEe;8.00 +Bernasconi Laurie;;Animer I (B);1ASEFEa, 1ASEFEd, 1ASEFEe;8.00 +Bernasconi Laurie;;Animer I (B);1ASEFEb, 1ASEFEc, 1ASEFEd, 1ASEFEf;8.00 +Bernasconi Laurie;;Animer I (B);1ASEFEb, 1ASEFEc, 1ASEFEd, 1ASEFEf;8.00 +Bernasconi Laurie;;Animer II (Bgén);2MPTS ASE1;8.00 +Bernasconi Laurie;;Animer II (Bgén);2MPTS ASE1;8.00 +Bernasconi Laurie;;Animer II (Bgén);2MPTS ASE1;8.00 +Bernasconi Laurie;;Animer II (Bgén);2MPTS ASE1;8.00 +Bernasconi Laurie;;Animer II (Bgén);2MPTS ASE1;8.00 +Bernasconi Laurie;;Colloque ASE;; +Bernasconi Laurie;;Conseil de classe;; +Bernasconi Laurie;;Conseil de classe;; +Bernasconi Laurie;;Formation AMOK EPC;; +Bernasconi Laurie;;Relations prof. (I);1ASEFEf, 3MPS ASE1;2.00 +Bernasconi Laurie;;Relations prof. (I);1MPTS ASE3, 3MPS ASE3;2.00 +Bernasconi Laurie;;Relations prof. (I);3MPS ASE1;8.00 +Bernasconi Laurie;;Relations prof. (I);3MPS ASE1;8.00 +Bernasconi Laurie;;Relations prof. (I);3MPS ASE3;8.00 +Bernasconi Laurie;;Remise des titres;; +Bernasconi Laurie;;Sém. enfance 2;[2ASEFE c-d E S2];8.00 +Bernasconi Laurie;;Sém. enfance 2;[2ASEFE c-d E S2];8.00 +Bernasconi Laurie;;Sém. enfance 2;[2ASEFE c-d E S2];8.00 +Bernasconi Laurie;;Sém. enfance 2;[2ASEFE c-d E S2];8.00 +Bernasconi Laurie;;Travail personnel;[2ASEFE c-d E S2];8.00 diff --git a/stages/tests.py b/stages/tests.py index f663232..add51c6 100644 --- a/stages/tests.py +++ b/stages/tests.py @@ -9,7 +9,7 @@ from django.utils.html import escape from .models import ( Level, Domain, Section, Klass, Period, Student, Corporation, Availability, - CorpContact, Referent, Training + CorpContact, Referent, Teacher, Training, ) from .utils import school_year @@ -157,7 +157,7 @@ class ImportTests(TestCase): path = os.path.join(os.path.dirname(__file__), 'test_files', 'EXPORT_GAN.xls') self.client.login(username='me', password='mepassword') with open(path, 'rb') as fh: - response = self.client.post(reverse('tabimport'), {'upload': fh}, follow=True) + response = self.client.post(reverse('import-students'), {'upload': fh}, follow=True) self.assertContains(response, escape("La classe '1ASEFEa' n'existe pas encore")) lev1 = Level.objects.create(name='1') @@ -172,5 +172,16 @@ class ImportTests(TestCase): level=lev1, ) with open(path, 'rb') as fh: - response = self.client.post(reverse('tabimport'), {'upload': fh}, follow=True) + response = self.client.post(reverse('import-students'), {'upload': fh}, follow=True) self.assertContains(response, "Created objects: 2") + + def test_import_hp(self): + teacher = Teacher.objects.create( + first_name='Laurie', last_name='Bernasconi', birth_date='1974-08-08' + ) + path = os.path.join(os.path.dirname(__file__), 'test_files', 'HYPERPLANNING.csv') + self.client.login(username='me', password='mepassword') + with open(path, 'rb') as fh: + response = self.client.post(reverse('import-hp'), {'upload': fh}, follow=True) + self.assertContains(response, "Created objects: 13, modified objects: 10") + self.assertEqual(teacher.course_set.count(), 13) diff --git a/stages/views.py b/stages/views.py index fee6e1f..d568b86 100644 --- a/stages/views.py +++ b/stages/views.py @@ -2,10 +2,11 @@ import json from collections import OrderedDict from datetime import date, datetime, timedelta -from tabimport import FileFactory +from tabimport import CSVImportedFile, FileFactory from django.conf import settings from django.contrib import messages +from django.core.files import File from django.db.models import Case, Count, When from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect from django.shortcuts import get_object_or_404 @@ -13,9 +14,11 @@ from django.urls import reverse from django.utils.translation import ugettext as _ from django.views.generic import DetailView, FormView, TemplateView, ListView -from .forms import PeriodForm, StudentImportForm -from .models import (Klass, Section, Student, Corporation, CorpContact, Period, - Training, Referent, Availability) +from .forms import PeriodForm, StudentImportForm, UploadHPFileForm +from .models import ( + Klass, Section, Student, Teacher, Corporation, CorpContact, Course, Period, + Training, Referent, Availability, +) def school_year_start(): @@ -282,13 +285,18 @@ def del_training(request): return HttpResponse(json.dumps({'ref_id': ref_id}), content_type="application/json") -class StudentImportView(FormView): - template_name = 'student_import.html' - form_class = StudentImportForm +class ImportViewBase(FormView): + template_name = 'file_import.html' def form_valid(self, form): + upfile = form.cleaned_data['upload'] try: - imp_file = FileFactory(form.cleaned_data['upload']) + if 'csv' in upfile.content_type: + # Reopen the file in text mode + upfile = open(upfile.temporary_file_path(), mode='r', encoding='utf-8-sig') + imp_file = CSVImportedFile(File(upfile)) + else: + imp_file = FileFactory(upfile) created, modified = self.import_data(imp_file) except Exception as e: if settings.DEBUG: @@ -299,6 +307,11 @@ class StudentImportView(FormView): 'cr': created, 'mod': modified}) return HttpResponseRedirect(reverse('admin:index')) + +class StudentImportView(ImportViewBase): + title = "Importation étudiants" + form_class = StudentImportForm + def import_data(self, up_file): """ Import Student data from uploaded file. """ student_mapping = settings.STUDENT_IMPORT_MAPPING @@ -334,6 +347,74 @@ class StudentImportView(FormView): return obj_created, obj_modified +class HPImportView(ImportViewBase): + """ + Importation du fichier HyperPlanning pour l'établissement des feuilles + de charges. + """ + form_class = UploadHPFileForm + mapping = { + 'NOMPERSO_ENS': 'teacher', + 'LIBELLE_MAT': 'subject', + 'NOMPERSO_DIP': 'klass', + 'TOTAL': 'period', + } + # Mapping between klass field and imputation + account_categories = { + 'ASAFE': 'ASA', + 'ASEFE': 'ASE', + 'ASSCFE': 'ASSC', + 'MP': 'LEP', + 'EDEpe': 'EDEpe', + 'EDEps': 'EDEps', + 'EDE': 'EDE', + 'EDS': 'EDS', + 'CAS-FPP': 'CAS-FPP', + 'Mandat_ASSC': 'ASSC', + 'Mandat_ASE': 'ASE', + 'Mandat_EDE': 'EDE', + 'Mandat_EDS': 'EDA', + } + + def import_data(self, up_file): + obj_created = obj_modified = 0 + + #Pour accélérer la recherche + profs = {} + for t in Teacher.objects.all(): + profs[t.__str__()] = t + Course.objects.all().delete() + + for line in up_file: + if (line['LIBELLE_MAT'] == '' or line['NOMPERSO_DIP'] == '' or + line['TOTAL'] == ''): + continue + defaults = { + 'teacher': profs[line['NOMPERSO_ENS']], + 'subject': line['LIBELLE_MAT'], + 'klass': line['NOMPERSO_DIP'], + } + + obj, created = Course.objects.get_or_create( + teacher = defaults['teacher'], + subject = defaults['subject'], + klass = defaults['klass']) + + period = int(float(line['TOTAL'])) + if created: + obj.period = period + obj_created += 1 + for k, v in self.account_categories.items(): + if k in obj.klass: + obj.imputation = v + break + else: + obj.period += period + obj_modified += 1 + obj.save() + return obj_created, obj_modified + + EXPORT_FIELDS = [ ('Prénom', 'student__first_name'), ('Nom', 'student__last_name'), ('ID externe', 'student__ext_id'), diff --git a/templates/admin/index.html b/templates/admin/index.html index 46fd726..59570bc 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -73,7 +73,8 @@

Importation/exportation

diff --git a/templates/student_import.html b/templates/file_import.html similarity index 91% rename from templates/student_import.html rename to templates/file_import.html index 726a95f..62d6284 100644 --- a/templates/student_import.html +++ b/templates/file_import.html @@ -3,7 +3,7 @@ {% block breadcrumbs %} {% endblock %}