Initial commit
This commit is contained in:
commit
793bb6a488
182 changed files with 17153 additions and 0 deletions
0
archive/__init__.py
Normal file
0
archive/__init__.py
Normal file
9
archive/admin.py
Normal file
9
archive/admin.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Archive
|
||||
|
||||
|
||||
@admin.register(Archive)
|
||||
class ArchiveAdmin(admin.ModelAdmin):
|
||||
list_display = ['nom', 'unite', 'date_debut', 'date_fin']
|
||||
search_fields = ['nom']
|
||||
31
archive/forms.py
Normal file
31
archive/forms.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from django import forms
|
||||
|
||||
from aemo.forms import BootstrapMixin
|
||||
|
||||
|
||||
class ArchiveKeyUploadForm(BootstrapMixin, forms.Form):
|
||||
file = forms.FileField(label='Clé de déchiffrement des archives', required=True)
|
||||
|
||||
|
||||
class ArchiveFilterForm(BootstrapMixin, forms.Form):
|
||||
|
||||
search_famille = forms.CharField(
|
||||
label='Recherche par nom de famille',
|
||||
max_length=30,
|
||||
required=False
|
||||
)
|
||||
|
||||
search_intervenant = forms.CharField(
|
||||
label='Recherche par interv. CRNE',
|
||||
max_length=30,
|
||||
required=False
|
||||
)
|
||||
|
||||
def filter(self, archives):
|
||||
if not self.cleaned_data['search_famille'] and not self.cleaned_data['search_intervenant']:
|
||||
return archives.none()
|
||||
if self.cleaned_data['search_famille']:
|
||||
archives = archives.filter(nom__icontains=self.cleaned_data['search_famille'])
|
||||
if self.cleaned_data['search_intervenant']:
|
||||
archives = archives.filter(intervenant__icontains=self.cleaned_data['search_intervenant'])
|
||||
return archives
|
||||
30
archive/migrations/0001_Add_archive_model.py
Normal file
30
archive/migrations/0001_Add_archive_model.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Archive',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nom', models.CharField(max_length=40)),
|
||||
('unite', models.CharField(max_length=10)),
|
||||
('intervenant', models.CharField(blank=True, max_length=50)),
|
||||
('ope', models.CharField(blank=True, max_length=50)),
|
||||
('motif_fin', models.CharField(blank=True, max_length=30)),
|
||||
('date_debut', models.DateField(null=True)),
|
||||
('date_fin', models.DateField()),
|
||||
('key', models.TextField()),
|
||||
('pdf', models.FileField(null=True, upload_to='archives/')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('nom',),
|
||||
},
|
||||
),
|
||||
]
|
||||
16
archive/migrations/0002_longer_intervenant.py
Normal file
16
archive/migrations/0002_longer_intervenant.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('archive', '0001_Add_archive_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='archive',
|
||||
name='intervenant',
|
||||
field=models.CharField(blank=True, max_length=120),
|
||||
),
|
||||
]
|
||||
0
archive/migrations/__init__.py
Normal file
0
archive/migrations/__init__.py
Normal file
19
archive/models.py
Normal file
19
archive/models.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Archive(models.Model):
|
||||
nom = models.CharField(max_length=40)
|
||||
unite = models.CharField(max_length=10)
|
||||
intervenant = models.CharField(max_length=120, blank=True)
|
||||
ope = models.CharField(max_length=50, blank=True)
|
||||
motif_fin = models.CharField(max_length=30, blank=True)
|
||||
date_debut = models.DateField(null=True)
|
||||
date_fin = models.DateField()
|
||||
key = models.TextField()
|
||||
pdf = models.FileField(upload_to='archives/', null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('nom',)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.unite}: {self.nom}"
|
||||
64
archive/pdf.py
Normal file
64
archive/pdf.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from pypdf import PdfWriter, PdfReader
|
||||
|
||||
from django.utils.text import slugify
|
||||
from aemo.pdf import BasePDF, BilanPdf, EvaluationPdf, MessagePdf, RapportPdf, JournalPdf
|
||||
|
||||
|
||||
class ArchiveBase(BasePDF):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.merger = PdfWriter()
|
||||
|
||||
def get_filename(self):
|
||||
return f"{slugify(self.instance.nom)}-{self.instance.pk}.pdf"
|
||||
|
||||
def append_pdf(self, PDFClass, obj):
|
||||
temp = BytesIO()
|
||||
pdf = PDFClass(temp, obj)
|
||||
pdf.produce()
|
||||
self.merger.append(temp)
|
||||
|
||||
def append_other_docs(self, documents):
|
||||
msg = []
|
||||
for doc in documents:
|
||||
doc_path = Path(doc.fichier.path)
|
||||
if not doc_path.exists():
|
||||
msg.append(f"Le fichier «{doc.titre}» n'existe pas!")
|
||||
continue
|
||||
if doc_path.suffix.lower() == '.pdf':
|
||||
self.merger.append(PdfReader(str(doc_path)))
|
||||
elif doc_path.suffix.lower() in ['.doc', '.docx']:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
cmd = ['libreoffice', '--headless', '--convert-to', 'pdf', '--outdir', tmpdir, doc_path]
|
||||
subprocess.run(cmd, capture_output=True)
|
||||
converted_path = Path(tmpdir) / f'{doc_path.stem}.pdf'
|
||||
if converted_path.exists():
|
||||
self.merger.append(PdfReader(str(converted_path)))
|
||||
else:
|
||||
msg.append(f"La conversion du fichier «{doc.titre}» a échoué")
|
||||
elif doc_path.suffix.lower() == '.msg':
|
||||
self.append_pdf(MessagePdf, doc)
|
||||
else:
|
||||
msg.append(f"Le format du fichier «{doc.titre}» ne peut pas être intégré.")
|
||||
return msg
|
||||
|
||||
|
||||
class ArchivePdf(ArchiveBase):
|
||||
title = "Archive"
|
||||
|
||||
def produce(self):
|
||||
famille = self.instance
|
||||
self.append_pdf(EvaluationPdf, famille)
|
||||
self.append_pdf(JournalPdf, famille)
|
||||
for bilan in famille.bilans.all():
|
||||
self.append_pdf(BilanPdf, bilan)
|
||||
for rapport in famille.rapports.all():
|
||||
self.append_pdf(RapportPdf, rapport)
|
||||
msg = self.append_other_docs(famille.documents.all())
|
||||
self.merger.write(self.doc.filename)
|
||||
return msg
|
||||
18
archive/templates/archive/key_upload.html
Normal file
18
archive/templates/archive/key_upload.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-4 mt-5 p-5 border">
|
||||
<form action="." method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_div }}
|
||||
<div id="actions" class="row border-top mt-4">
|
||||
<div class="col mt-3 text-end">
|
||||
<a class="btn btn-sm btn-secondary" href="javascript: history.go(-1)">Annuler</a>
|
||||
<button class="btn btn-sm btn-success" name="save" type="submit">Envoyer et déchiffrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
37
archive/templates/archive/list.html
Normal file
37
archive/templates/archive/list.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block extra_javascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$("#id_search_famille, #id_search_intervenant").keyup(debounce((ev) => {
|
||||
const form = ev.target.form;
|
||||
$.getJSON({
|
||||
url: form.action,
|
||||
data: $(form).serialize()
|
||||
}).done(function(response) {
|
||||
$("#archive_table").html(response);
|
||||
});
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-5 border-bottom">
|
||||
<div class="col-2 pt-5">
|
||||
<h4>Archives {{ unite|upper }}</h4>
|
||||
</div>
|
||||
<div class="col">
|
||||
<form method="get" action="{% url 'archive-list' unite %}">
|
||||
<div class="row justify-content-end mb-3">
|
||||
<div class="col-auto">{{ form.search_famille.label_tag }} {{ form.search_famille }}</div>
|
||||
<div class="col-auto">{{form.search_intervenant.label_tag }} {{ form.search_intervenant }}</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div id="archive_table">Utilisez la recherche par nom de famille et/ou par intervenant.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
archive/templates/archive/list_partial.html
Normal file
28
archive/templates/archive/list_partial.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<table class="table table-sm table-hover">
|
||||
<tr>
|
||||
<th>Nom</th><th>Réf. {{ unite|upper }}</th><th>Réf. OPE</th><th>Date de début</th>
|
||||
<th>Date de fin</th><th>Motif de fin</th>
|
||||
{% if can_download %}
|
||||
<th width="15rem">Déchiffrement</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for archive in archives %}
|
||||
<tr>
|
||||
<td>{{ archive.nom }}</td>
|
||||
<td>{{ archive.intervenant }}</td>
|
||||
<td>{{ archive.ope }}</td>
|
||||
<td>{{ archive.date_debut|default:'' }}</td>
|
||||
<td>{{ archive.date_fin }}</td>
|
||||
<td>{{ archive.motif_fin }}</td>
|
||||
{% if can_download %}
|
||||
<td align="center">
|
||||
<a class="btn btn-sm bg-success" href="{%url 'archive-decrypt' archive.pk %}">Déchiffrer</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7">Aucune famille pour cette recherche.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
0
archive/tests/__init__.py
Normal file
0
archive/tests/__init__.py
Normal file
51
archive/tests/crne_rsa
Normal file
51
archive/tests/crne_rsa
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
1
archive/tests/crne_rsa.pub
Normal file
1
archive/tests/crne_rsa.pub
Normal file
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCsGKEEl1oiJgE5o/8CPC+pBAU7WKdUQhWsq6Q8mZvEyMVx4Gsd3VgMxDYnkr0tyqVmcUkufVfAZV8ZIyixEDxU4pqh/8QD2el1D+dw2lNzwa6CrbPRSaxiOvY5eWhm7duQR3OETs1/nAT+zmodDogWfo3IfBJuDqJRWXV34L5Rk1Vdk9rKEAfGyogxBvtr8UKq6xZXSDweY/jFKrtnyACXsGmD94aqcgpWbaLxTR064Ahyuh/jlACfTt7aKZf/YYtJRGseeNlx6VRRYl+UQN02uwWL/fPU9g+E04e/BHtIvPByddnwYSWlwaeIFHS9Ta6sIO0LW3E1oYAGOhao8c+Z3GyqG6Og13x5XG7bQOjR/IqtKcWobNT+cXJ7vtAkzsdQpIIgtFIm9eGNXhGfuq9Tn4KlzzAl3lP9y+RpchnRxcZJTqslflB0tFPTkwj5biDIbNvcxOdr38QyO2lHyejpJgFixxNj+PD0reE1ii7UyMkXXLelRKZwHRpetSqNzVuyHOrtJW+R2gMbPaPRsfI6Rz3gbXIw0m4rrOeoepnMGMT0w90TRF0ohqNACrFx/7dLyFL6529gx8DmBntWLUWIYkGJ/o9F1Iwzlp3s3e0KWvsktMfyadgMbIQtolqXaVS7rpIs4uJBZjR1Ya2+MY/IyFZbNJM+ny3aYxGMvHcUhQ== transit@example.org
|
||||
BIN
archive/tests/sample-2.msg
Normal file
BIN
archive/tests/sample-2.msg
Normal file
Binary file not shown.
BIN
archive/tests/sample.doc
Normal file
BIN
archive/tests/sample.doc
Normal file
Binary file not shown.
BIN
archive/tests/sample.docx
Normal file
BIN
archive/tests/sample.docx
Normal file
Binary file not shown.
BIN
archive/tests/sample.msg
Normal file
BIN
archive/tests/sample.msg
Normal file
Binary file not shown.
BIN
archive/tests/sample.pdf
Normal file
BIN
archive/tests/sample.pdf
Normal file
Binary file not shown.
204
archive/tests/tests.py
Normal file
204
archive/tests/tests.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import os.path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.files import File
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from aemo.models import (
|
||||
Bilan, Document, Famille, LibellePrestation, Personne, Role, Prestation,
|
||||
Rapport, Utilisateur
|
||||
)
|
||||
from aemo.tests import InitialDataMixin, TempMediaRootMixin
|
||||
from aemo.utils import format_d_m_Y
|
||||
from ..models import Archive
|
||||
|
||||
public_key_path = os.path.join(settings.BASE_DIR, 'archive/tests/crne_rsa.pub')
|
||||
|
||||
|
||||
@override_settings(CRNE_RSA_PUBLIC_KEY=public_key_path)
|
||||
class ArchiveTests(InitialDataMixin, TempMediaRootMixin, TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = Utilisateur.objects.create_user('user', 'user@example.org', sigle='XX')
|
||||
self.create_kwargs = {
|
||||
'nom': 'John Doe',
|
||||
'unite': 'aemo',
|
||||
'date_debut': date(2021, 1, 1),
|
||||
'date_fin': date(2021, 12, 31),
|
||||
'motif_fin': 'Autre',
|
||||
'key': '',
|
||||
'pdf': 'encrypted_data'
|
||||
}
|
||||
|
||||
def _create_archive(self):
|
||||
fam = Famille.objects.create_famille(nom='Haddock', equipe='aemo')
|
||||
fam.suivi.date_fin_suivi = date(2019, 1, 1)
|
||||
fam.suivi.motif_fin_suivi = 'autres'
|
||||
fam.suivi.save()
|
||||
for idx, doc_name in enumerate(['sample.docx', 'sample.doc', 'sample.pdf', 'sample.msg']):
|
||||
doc = Document(famille=fam, titre=f"Test {idx}")
|
||||
with (Path(__file__).parent / doc_name).open(mode='rb') as fh:
|
||||
doc.fichier = File(fh, name=doc_name)
|
||||
doc.save()
|
||||
|
||||
self.user.user_permissions.add(Permission.objects.get(codename='can_archive'))
|
||||
self.client.force_login(self.user)
|
||||
self.client.post(reverse('archive-add', args=['aemo', fam.pk]))
|
||||
return Archive.objects.get(nom='Haddock', unite='aemo')
|
||||
|
||||
def test_model_creation(self):
|
||||
arch = Archive.objects.create(**self.create_kwargs)
|
||||
self.assertEqual(arch.nom, 'John Doe')
|
||||
self.assertEqual(arch.unite, 'aemo')
|
||||
self.assertEqual(arch.date_debut, date(2021, 1, 1))
|
||||
self.assertEqual(arch.date_fin, date(2021, 12, 31))
|
||||
self.assertEqual(arch.pdf, 'encrypted_data')
|
||||
|
||||
def test_sans_permission_d_archiver(self):
|
||||
fam = Famille.objects.create_famille(nom='Haddock', equipe='aemo')
|
||||
fam.suivi.date_fin_suivi = date(2019, 1, 1)
|
||||
fam.suivi.motif_fin_suivi = 'autre'
|
||||
fam.suivi.save()
|
||||
self.assertEqual(fam.can_be_archived(self.user), False)
|
||||
|
||||
def test_avec_permission_d_archiver(self):
|
||||
fam = Famille.objects.create_famille(nom='Haddock', equipe='aemo')
|
||||
fam.suivi.date_fin_suivi = date(2019, 1, 1)
|
||||
fam.suivi.motif_fin_suivi = 'autre'
|
||||
fam.suivi.save()
|
||||
self.user.user_permissions.add(Permission.objects.get(codename='can_archive'))
|
||||
self.assertEqual(fam.can_be_archived(self.user), True)
|
||||
|
||||
def test_archivage_aemo(self):
|
||||
famille = Famille.objects.create_famille(
|
||||
nom='Doe', equipe='aemo', rue="Rue du lac", npa='2000', localite='Paris', telephone='012 345 67 89')
|
||||
famille.suivi.date_fin_suivi = date.today() - timedelta(days=700)
|
||||
famille.suivi.motif_fin_suivi = 'autre'
|
||||
famille.suivi.save()
|
||||
file_paths = []
|
||||
for idx, doc_name in enumerate(['sample.docx', 'sample.doc', 'sample.pdf', 'sample.msg', 'sample-2.msg']):
|
||||
doc = Document(famille=famille, titre=f"Test {idx}")
|
||||
with (Path(__file__).parent / doc_name).open(mode='rb') as fh:
|
||||
doc.fichier = File(fh, name=doc_name)
|
||||
doc.save()
|
||||
file_paths.append(doc.fichier.path)
|
||||
Personne.objects.create_personne(
|
||||
famille=famille, prenom='Archibald', nom='Doe', role=Role.objects.get(nom='Père')
|
||||
)
|
||||
enfant = Personne.objects.create_personne(
|
||||
famille=famille, prenom='Gaston', nom='Doe', date_naissance=date.today() - timedelta(days=720),
|
||||
role=Role.objects.get(nom='Enfant suivi')
|
||||
)
|
||||
enfant.formation.creche = 'Les Schtroumpfs'
|
||||
enfant.formation.save()
|
||||
Bilan.objects.create(famille=famille, date=date.today())
|
||||
Rapport.objects.create(famille=famille, date=date.today(), auteur=self.user)
|
||||
prest = Prestation.objects.create(
|
||||
famille=famille, auteur=self.user, date_prestation=date.today(), duree=timedelta(hours=2),
|
||||
lib_prestation=LibellePrestation.objects.first())
|
||||
prest.intervenants.add(self.user, through_defaults={'role': Role.objects.get(nom='Référent')})
|
||||
with (Path(__file__).parent / 'sample.pdf').open(mode='rb') as fh:
|
||||
prest.fichier = File(fh, name=doc_name)
|
||||
prest.save()
|
||||
file_paths.append(prest.fichier.path)
|
||||
|
||||
grp = Group.objects.get(name='aemo')
|
||||
self.user.groups.add(grp)
|
||||
self.user.user_permissions.add(
|
||||
*list(Permission.objects.filter(codename__in=['view_famille', 'change_famille', 'can_archive']))
|
||||
)
|
||||
self.assertTrue(famille.can_be_archived(self.user))
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(reverse('archive-add', args=['aemo', famille.pk]))
|
||||
self.assertRedirects(response, reverse('suivis-termines'))
|
||||
|
||||
famille.refresh_from_db()
|
||||
self.assertNotEqual(famille.nom, 'Doe')
|
||||
self.assertEqual(len(famille.nom), 10)
|
||||
self.assertEqual(famille.rue, '')
|
||||
self.assertEqual(famille.npa, '2000')
|
||||
self.assertEqual(famille.localite, 'Paris'),
|
||||
self.assertEqual(famille.telephone, '')
|
||||
self.assertEqual(famille.parents(), [])
|
||||
|
||||
enfant.refresh_from_db()
|
||||
self.assertNotEqual(enfant.nom, 'Doe')
|
||||
self.assertEqual(len(famille.nom), 10)
|
||||
self.assertNotEqual(enfant.prenom, 'Gaston')
|
||||
self.assertEqual(len(famille.nom), 10)
|
||||
self.assertEqual(enfant.formation.creche, '')
|
||||
|
||||
self.assertEqual(famille.documents.count(), 0)
|
||||
self.assertEqual(famille.bilans.count(), 0)
|
||||
self.assertEqual(famille.rapports.count(), 0)
|
||||
for prest in famille.prestations.all():
|
||||
self.assertEqual(prest.texte, '')
|
||||
self.assertEqual(prest.duree, timedelta(hours=2))
|
||||
self.assertEqual(bool(prest.fichier), False)
|
||||
|
||||
for path in file_paths:
|
||||
self.assertFalse(Path(path).exists())
|
||||
|
||||
arch = Archive.objects.get(nom='Doe', unite='aemo')
|
||||
self.assertTrue(os.path.exists(arch.pdf.path))
|
||||
self.assertIn(f"aemo/doe-{famille.pk}", arch.pdf.name)
|
||||
self.assertTrue(famille.prestations.exists())
|
||||
|
||||
# Cannot be archived a second time:
|
||||
response = self.client.post(reverse('archive-add', args=['aemo', famille.pk]))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_decryptage_access(self):
|
||||
arch = self._create_archive()
|
||||
private_key = os.path.join(settings.BASE_DIR, 'archive/tests/crne_rsa')
|
||||
anonymous = Utilisateur.objects.create_user('anonymous', email='anonymous@example.org')
|
||||
self.client.force_login(anonymous)
|
||||
with open(private_key, 'rb') as f:
|
||||
response = self.client.post(reverse('archive-decrypt', args=[arch.pk]), data={'file': f})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
anonymous.user_permissions.add(Permission.objects.get(codename='can_archive'))
|
||||
self.client.force_login(anonymous)
|
||||
with open(private_key, 'rb') as f:
|
||||
response = self.client.post(reverse('archive-decrypt', args=[arch.pk]), data={'file': f})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_decryptage_aemo(self):
|
||||
arch = self._create_archive()
|
||||
private_key = os.path.join(settings.BASE_DIR, 'archive/tests/crne_rsa')
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(reverse('archive-decrypt', args=[arch.pk]), data={'file': ''})
|
||||
self.assertEqual(response.context['form'].errors, {'file': ['Ce champ est obligatoire.']})
|
||||
|
||||
with open(private_key, 'rb') as f:
|
||||
response = self.client.post(reverse('archive-decrypt', args=[arch.pk]), data={'file': f})
|
||||
self.assertEqual(
|
||||
response.get('Content-Disposition'),
|
||||
f"attachment; filename={slugify(arch.nom)}.pdf"
|
||||
)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=True, mode='wb') as fh:
|
||||
fh.write(response.content)
|
||||
subprocess.run(['pdftotext', fh.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
with open(f'{fh.name}.txt', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
self.assertIn('Historique', content)
|
||||
self.assertIn('Fin du suivi le :\n\n01.01.2019', content)
|
||||
self.assertIn('Motif de fin de suivi :\n\nAutres', content)
|
||||
self.assertIn(f"Date d'archivage :\n\n{format_d_m_Y(date.today())}", content)
|
||||
self.assertIn('Famille Haddock', content)
|
||||
self.assertIn('Informations', content)
|
||||
self.assertIn("Fichier docx d’exemple", content)
|
||||
self.assertIn("Exemple de fichier doc", content)
|
||||
self.assertIn("Fichier pdf d’exemple", content)
|
||||
self.assertIn("Kind regards", content)
|
||||
9
archive/urls.py
Normal file
9
archive/urls.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path
|
||||
|
||||
from archive import views
|
||||
|
||||
urlpatterns = [
|
||||
path('<str:unite>/famille/<int:pk>/add/', views.ArchiveCreateView.as_view(), name='archive-add'),
|
||||
path('<str:unite>/list/', views.ArchiveListView.as_view(), name='archive-list'),
|
||||
path('<int:pk>/decrypt/', views.ArchiveDecryptView.as_view(), name='archive-decrypt'),
|
||||
]
|
||||
153
archive/views.py
Normal file
153
archive/views.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files.base import ContentFile
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, reverse
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
from django.views.generic import FormView, TemplateView, View
|
||||
|
||||
from aemo.models import Famille
|
||||
from aemo.utils import is_ajax
|
||||
from .models import Archive
|
||||
from .pdf import ArchivePdf
|
||||
|
||||
from .forms import ArchiveFilterForm, ArchiveKeyUploadForm
|
||||
|
||||
|
||||
class ArchiveCreateView(View):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
unite = 'aemo'
|
||||
temp = BytesIO()
|
||||
|
||||
famille = get_object_or_404(Famille, pk=kwargs['pk'], archived_at__isnull=True)
|
||||
intervenants_list = '/'.join([f"{interv.nom} {interv.prenom[0].upper()}."
|
||||
for interv in famille.suivi.intervenants.all()])
|
||||
ope_list = famille.suivi.ope_referents
|
||||
motif_fin = famille.suivi.get_motif_fin_suivi_display()
|
||||
pdf = ArchivePdf(temp, famille)
|
||||
|
||||
if not famille.can_be_archived(self.request.user):
|
||||
raise PermissionDenied("Vous n'avez pas les droits nécessaires pour accéder à cette page.")
|
||||
|
||||
famille.archived_at = timezone.now()
|
||||
pdf.produce()
|
||||
filename = f"{unite}/{pdf.get_filename()}"
|
||||
temp.seek(0)
|
||||
pdf = temp.read()
|
||||
|
||||
# Create a symmetric Fernet key to encrypt the PDF, and encrypt that key with asymmetric RSA key.
|
||||
key = Fernet.generate_key()
|
||||
fernet = Fernet(key)
|
||||
pdf_crypted = fernet.encrypt(pdf)
|
||||
|
||||
with open(settings.CRNE_RSA_PUBLIC_KEY, "rb") as key_file:
|
||||
public_key = serialization.load_ssh_public_key(key_file.read())
|
||||
padd = padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA1()), algorithm=hashes.SHA1(), label=None)
|
||||
fernet_crypted = public_key.encrypt(key, padding=padd)
|
||||
|
||||
nom_famille = famille.nom
|
||||
arch = Archive.objects.create(
|
||||
nom=nom_famille,
|
||||
unite=unite,
|
||||
intervenant=intervenants_list,
|
||||
ope=", ".join([ope.nom_prenom for ope in ope_list]),
|
||||
motif_fin=motif_fin,
|
||||
date_debut=famille.suivi.date_debut_suivi,
|
||||
date_fin=famille.suivi.date_fin_suivi,
|
||||
key=base64.b64encode(fernet_crypted).decode(),
|
||||
pdf=None
|
||||
)
|
||||
arch.pdf.save(filename, ContentFile(pdf_crypted))
|
||||
famille.archived_at = timezone.now()
|
||||
famille.save()
|
||||
|
||||
famille.anonymiser()
|
||||
|
||||
if is_ajax(request):
|
||||
return JsonResponse({'id': famille.pk}, safe=True)
|
||||
|
||||
messages.success(request, f"La famille «{nom_famille}» a bien été archivée.")
|
||||
return HttpResponseRedirect(reverse("suivis-termines"))
|
||||
|
||||
|
||||
class ArchiveListView(TemplateView):
|
||||
template_name = 'archive/list.html'
|
||||
model = Archive
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.unite = self.kwargs['unite']
|
||||
self.filter_form = ArchiveFilterForm(data=request.GET or None)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if is_ajax(request) and self.unite == 'aemo':
|
||||
if self.filter_form.is_bound and self.filter_form.is_valid():
|
||||
archives = self.filter_form.filter(Archive.objects.filter(unite=self.unite))
|
||||
else:
|
||||
archives = Archive.objects.none()
|
||||
response = render_to_string(
|
||||
template_name='archive/list_partial.html',
|
||||
context={
|
||||
'archives': archives,
|
||||
'can_download': request.user.has_perm('aemo.can_archive'),
|
||||
}
|
||||
)
|
||||
return JsonResponse(response, safe=False)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
return {
|
||||
**super().get_context_data(*args, **kwargs),
|
||||
'unite': self.unite,
|
||||
'form': self.filter_form,
|
||||
'archives': Archive.objects.filter(unite=self.unite),
|
||||
'can_download': self.request.user.has_perm('aemo.can_archive'),
|
||||
}
|
||||
|
||||
|
||||
class ArchiveDecryptView(PermissionRequiredMixin, FormView):
|
||||
form_class = ArchiveKeyUploadForm
|
||||
template_name = 'archive/key_upload.html'
|
||||
permission_required = 'aemo.can_archive'
|
||||
|
||||
def form_valid(self, form):
|
||||
arch = get_object_or_404(Archive, pk=self.kwargs['pk'])
|
||||
try:
|
||||
with open(arch.pdf.path, "rb") as fh:
|
||||
pdf_crypted = fh.read()
|
||||
except OSError as err:
|
||||
messages.error(self.request, f"Erreur lors de la lecture du document ({str(err)})")
|
||||
return HttpResponseRedirect(reverse('archive-list', args=[arch.unite]))
|
||||
|
||||
try:
|
||||
private_key_file = self.request.FILES['file'].read()
|
||||
private_key = serialization.load_pem_private_key(private_key_file, password=None)
|
||||
padd = padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA1()), algorithm=hashes.SHA1(), label=None)
|
||||
sim_key = private_key.decrypt(base64.b64decode(arch.key), padding=padd)
|
||||
|
||||
fernet = Fernet(sim_key)
|
||||
pdf_content = fernet.decrypt(pdf_crypted)
|
||||
except ValueError as err:
|
||||
messages.error(self.request, f"Erreur lors de la lecture de la clé ({str(err)})")
|
||||
return HttpResponseRedirect(reverse('archive-list', args=[arch.unite]))
|
||||
except InvalidToken:
|
||||
messages.error(self.request, "Erreur lors du déchiffrement")
|
||||
return HttpResponseRedirect(reverse('archive-list', args=[arch.unite]))
|
||||
|
||||
filename = f"{slugify(arch.nom)}.pdf"
|
||||
response = HttpResponse(pdf_content, content_type='application/pdf')
|
||||
response['Content-Disposition'] = "attachment; filename=%s" % filename
|
||||
return response
|
||||
Loading…
Add table
Add a link
Reference in a new issue