passerelle/passerelle/apps/cartads_cs/models.py

446 lines
18 KiB
Python

# -*- coding: utf-8 -*-
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import datetime
from ftplib import FTP
import json
import os
from xml.etree import ElementTree as etree
import zipfile
from Crypto.Cipher import AES
from django.core.files.storage import default_storage
from django.core.signing import Signer
from django.core.urlresolvers import reverse
from django.db import models
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _
from django.utils.six.moves.urllib import parse as urlparse
from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint
def cartads_file_location(instance, filename):
return 'cartads_cs/%s/%s' % (instance.tracking_code, filename)
class CartaDSFile(models.Model):
tracking_code = models.CharField(max_length=20)
id_piece = models.CharField(max_length=20)
uploaded_file = models.FileField(upload_to=cartads_file_location)
last_update_datetime = models.DateTimeField(auto_now=True)
class CartaDSDossier(models.Model):
email = models.CharField(max_length=256)
tracking_code = models.CharField(max_length=20)
commune_id = models.CharField(max_length=20)
type_dossier_id = models.CharField(max_length=20)
objet_demande_id = models.CharField(max_length=20, null=True)
zip_ready = models.BooleanField(default=False)
zip_sent = models.BooleanField(default=False)
zip_ack_response = models.CharField(null=True, max_length=20)
notification_url = models.URLField(null=True)
notification_message = models.TextField(null=True)
cartads_id_dossier = models.CharField(max_length=50, null=True)
cartads_numero_dossier = models.CharField(max_length=50, null=True)
last_update_datetime = models.DateTimeField(auto_now=True)
class CartaDSCS(BaseResource):
category = _('Misc')
wsdl_base_url = models.URLField(_('WSDL Base URL'),
help_text=_('ex: https://example.net/adscs/webservices/'))
username = models.CharField(_('Username'), max_length=64)
password = models.CharField(_('Password'), max_length=64)
iv = models.CharField(_('Initialisation Vector'), max_length=16)
secret_key = models.CharField(_('Secret Key'), max_length=16)
ftp_server = models.CharField(_('FTP Server'), max_length=128)
ftp_username = models.CharField(_('FTP Username'), max_length=64)
ftp_password = models.CharField(_('FTP Password'), max_length=64)
ftp_client_name = models.CharField(_('FTP Client Name'), max_length=64)
class Meta:
verbose_name = 'Cart@DS CS'
@property
def wsdl_url(self):
return self.get_wsdl_url()
def get_wsdl_url(self, service_type='ServicePortail'):
return self.wsdl_base_url + service_type + '.svc?singleWsdl'
def soap_client(self, **kwargs):
client = super(CartaDSCS, self).soap_client(**kwargs)
# fix URL that should have been changed by reverse proxy
parsed_wsdl_address = urlparse.urlparse(client.service._binding_options['address'])
parsed_real_address = urlparse.urlparse(self.wsdl_base_url)
client.service._binding_options['address'] = urlparse.urlunparse(
parsed_real_address[:2] + parsed_wsdl_address[2:])
return client
def get_token(self):
token_data = {
'date': datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S'),
'login': self.username,
'password': self.password,
}
token_data_str = json.dumps(token_data)
data_pad = AES.block_size - len(token_data_str) % AES.block_size
aes = AES.new(self.secret_key, AES.MODE_CBC, self.iv)
token = aes.encrypt(token_data_str + (chr(data_pad)*data_pad))
return base64.encodestring(token).replace('\n', '').rstrip('=')
def check_status(self):
self.soap_client().service.GetCommunes(self.get_token())
# description of common endpoint parameters
COMMUNE_ID_PARAM = {
'description': _('Identifier of collectivity'),
'example_value': '2'
}
TYPE_DOSSIER_ID_PARAM = {
'description': _('Identifier of file type'),
'example_value': 'CU',
}
OBJET_DEMANDE_ID_PARAM = {
'description': _('Identifier of demand subject'),
'example_value': '1',
}
TRACKING_CODE_PARAM = {
'description': _('Unique identifier (ex: tracking code)'),
'example_value': 'XCBTFRML',
}
PIECE_ID_PARAM = {
'description': _('Identifier of single file item'),
}
UPLOAD_TOKEN_PARAM = {
'description': _('Token for upload file'),
}
@endpoint(description=_('Get list of collectivities'))
def communes(self, request):
client = self.soap_client()
resp = client.service.GetCommunes(self.get_token())
return {'data': [{'id': str(x['Key']), 'text': x['Value']} for x in resp]}
@endpoint(description=_('Get lisf of file types'),
parameters={
'commune_id': COMMUNE_ID_PARAM
})
def types_dossier(self, request, commune_id):
client = self.soap_client()
resp = client.service.GetTypesDossier(self.get_token(), int(commune_id))
return {'data': [{'id': x['Key'], 'text': x['Value']} for x in resp]}
@endpoint(description=_('Get list of demand subjects'),
parameters={'type_dossier_id': TYPE_DOSSIER_ID_PARAM},
)
def objets_demande(self, request, type_dossier_id):
client = self.soap_client()
resp = client.service.GetObjetsDemande(self.get_token(), type_dossier_id)
return {'data': [{'id': str(x['Key']), 'text': x['Value']} for x in resp]}
@endpoint(description=_('Get list of CERFA documents'),
parameters={
'type_dossier_id': TYPE_DOSSIER_ID_PARAM,
'type_compte': {'description': _('Type of account')},
})
def liste_pdf(self, request, type_dossier_id, type_compte=1):
client = self.soap_client()
resp = client.service.GetListePdf(self.get_token(), type_dossier_id,
{'TypeCompteUtilisateur': type_compte})
return {'data': [
{'id': x['Identifiant'],
'text': u'%s: %s' % (x['Nom'], x['Description']),
'url': x['UrlTelechargement'],
} for x in resp]}
@endpoint(perm='can_access',
description=_('Get list of file items'),
parameters={
'type_dossier_id': TYPE_DOSSIER_ID_PARAM,
'objet_demande_id': OBJET_DEMANDE_ID_PARAM,
'tracking_code': TRACKING_CODE_PARAM,
})
def pieces(self, request, type_dossier_id, objet_demande_id, tracking_code):
client = self.soap_client()
resp = client.service.GetPieces(self.get_token(), type_dossier_id,
objet_demande_id)
signer = Signer(salt='cart@ds_cs')
upload_token = signer.sign(tracking_code)
cerfa_pieces = [
{'id': 'cerfa-%s-%s' % (type_dossier_id, objet_demande_id),
'text': 'Cerfa rempli',
'description': '',
'codePiece': '',
'reglementaire': True,
'files': [],
'max_files': 1,
'section_start': 'Cerfa',
},
{'id': 'cerfa-autres-%s-%s' % (type_dossier_id, objet_demande_id),
'text': 'Cerfa demandeurs complémentaires',
'description': '',
'codePiece': '',
'reglementaire': False,
'files': [],
'max_files': 6,
}
]
pieces = [
{'id': str(x['IdPiece']),
'text': x['Libelle'],
'description': x['Descriptif'],
'codePiece': x['CodePiece'],
'reglementaire': x['Reglementaire'],
'files': [],
'max_files': 6,
} for x in resp]
required_pieces = [x for x in pieces if x['reglementaire']]
if required_pieces:
required_pieces[0]['section_start'] = 'Pièces réglementaires'
optional_pieces = [x for x in pieces if not x['reglementaire']]
if optional_pieces:
optional_pieces[0]['section_start'] = 'Pièces spécifiques'
pieces = cerfa_pieces + required_pieces + optional_pieces
known_files = CartaDSFile.objects.filter(tracking_code=tracking_code)
for piece in pieces:
if request:
upload_url = request.build_absolute_uri('%supload/%s/%s/' % (
self.get_absolute_url(),
piece['id'],
upload_token))
else:
upload_url = None
piece['files'] = [
{
'url': upload_url,
'name': os.path.basename(x.uploaded_file.name),
'token': signer.sign(str(x.id)),
'id': x.id,
} for x in known_files if x.id_piece == str(piece['id'])]
if len(piece['files']) < piece['max_files']:
piece['files'].append({'url': upload_url})
return {'data': pieces}
@endpoint(perm='can_access',
description=_('Check list of file items'),
parameters={
'type_dossier_id': TYPE_DOSSIER_ID_PARAM,
'objet_demande_id': OBJET_DEMANDE_ID_PARAM,
'tracking_code': TRACKING_CODE_PARAM,
})
def check_pieces(self, request, type_dossier_id, objet_demande_id, tracking_code):
pieces = self.pieces(request, type_dossier_id, objet_demande_id, tracking_code)
result = True
for piece in pieces['data']:
if not piece['reglementaire']:
continue
if not [x for x in piece['files'] if x.get('name')]:
result = False
break
return {'result': result}
@endpoint(methods=['post'],
pattern='^(?P<id_piece>[\w-]+)/(?P<token>[\w:_-]+)/$',
description=_('Upload a single document file'),
parameters={
'id_piece': PIECE_ID_PARAM,
'token': UPLOAD_TOKEN_PARAM,
})
def upload(self, request, id_piece, token, **kwargs):
signer = Signer(salt='cart@ds_cs')
tracking_code = signer.unsign(token)
file_upload = CartaDSFile(
tracking_code=tracking_code,
id_piece=id_piece,
uploaded_file=request.FILES['files[]'])
file_upload.save()
return [{'name': os.path.basename(file_upload.uploaded_file.name),
'token': signer.sign(str(file_upload.id))}]
@endpoint(methods=['post'],
name='upload',
pattern='^(?P<id_piece>[\w-]+)/(?P<token>[\w:_-]+)/(?P<file_upload>[\w:_-]+)/delete/$',
description=_('Delete a single document file'),
parameters={
'id_piece': PIECE_ID_PARAM,
'token': UPLOAD_TOKEN_PARAM,
'file_upload': {
'description': _('Signed identifier of single document upload'),
},
})
def upload_delete(self, request, id_piece, token, file_upload, **kwargs):
# this cannot be verb DELETE as we have no way to set
# Access-Control-Allow-Methods
signer = Signer(salt='cart@ds_cs')
tracking_code = signer.unsign(token)
CartaDSFile.objects.filter(id=signer.unsign(file_upload)).delete()
return {'err': 0}
@endpoint(perm='can_access',
description=_('Validate and send a file'),
parameters={
'commune_id': COMMUNE_ID_PARAM,
'type_dossier_id': TYPE_DOSSIER_ID_PARAM,
'objet_demande_id': OBJET_DEMANDE_ID_PARAM,
'tracking_code': TRACKING_CODE_PARAM,
'email': {
'description': _('Email of requester'),
},
})
def send(self, request, commune_id, type_dossier_id, objet_demande_id, tracking_code, email):
dossier = CartaDSDossier(
commune_id=commune_id,
type_dossier_id=type_dossier_id,
objet_demande_id=objet_demande_id,
tracking_code=tracking_code,
email=email,
)
dossier.save()
signer = Signer(salt='cart@ds_cs/dossier')
dossier.notification_url = request.build_absolute_uri(
reverse('generic-endpoint', kwargs={
'connector': 'cartads-cs',
'slug': self.slug,
'endpoint': 'notification'})) + '/%s/' % signer.sign(str(dossier.id))
dossier.save()
self.add_job('pack', dossier_id=dossier.id)
return {'err': 0, 'dossier_id': dossier.id}
def pack(self, dossier_id):
dossier = CartaDSDossier.objects.get(id=dossier_id)
zip_filename = os.path.join(default_storage.path('cartads_cs'), '%s.zip' % dossier.tracking_code)
zip_file = zipfile.ZipFile(zip_filename, mode='w')
liste_pdf = self.liste_pdf(None, dossier.type_dossier_id)
cerfa_id = liste_pdf['data'][0]['id'].replace('*', '-')
pieces = self.pieces(None, dossier.type_dossier_id, dossier.objet_demande_id, dossier.tracking_code)
for piece in pieces['data']:
cnt = 1
for file in piece['files']:
if not file.get('id'):
continue
cartads_file = CartaDSFile.objects.get(id=file['id'])
if piece['id'] == 'cerfa-%s-%s' % (dossier.type_dossier_id, dossier.objet_demande_id):
zip_file.write(
cartads_file.uploaded_file.path,
'%s.pdf' % cerfa_id)
elif piece['id'].startswith('cerfa-autres-'):
zip_file.write(
cartads_file.uploaded_file.path,
'Fiches_complementaires/Cerfa_autres_demandeurs_%d.pdf' % cnt)
else:
zip_file.write(
cartads_file.uploaded_file.path,
'Pieces/%s-%s%s%s' % (
piece['id'],
piece['codePiece'],
cnt,
os.path.splitext(cartads_file.uploaded_file.path)[-1]))
cnt += 1
zip_file.close()
dossier.zip_ready = True
dossier.save()
self.add_job('send_to_cartads', dossier_id=dossier.id)
def send_to_cartads(self, dossier_id):
dossier = CartaDSDossier.objects.get(id=dossier_id)
zip_filename = os.path.join(default_storage.path('cartads_cs'), '%s.zip' % dossier.tracking_code)
ftp = FTP(self.ftp_server)
ftp.login(self.ftp_username, self.ftp_password)
ftp.cwd(self.ftp_client_name)
ftp.storbinary(
'STOR %s' % os.path.basename(zip_filename),
open(zip_filename))
ftp.quit()
def key_value_of_stringstring(d):
return {'KeyValueOfstringstring': [{'Key': x, 'Value': y} for x, y in d.items()]}
client = self.soap_client()
resp = client.service.NotifierDepotDossier(
self.get_token(),
dossier.commune_id,
dossier.type_dossier_id,
os.path.basename(zip_filename),
dossier.email,
key_value_of_stringstring(
{
'TraitementImmediat': 1,
'UrlNotification': dossier.notification_url,
}))
dossier.zip_sent = True
dossier.zip_ack_response = str(resp)
dossier.save()
@endpoint(pattern='^(?P<signed_dossier_id>[\w:_-]+)/$',
methods=['post'],
description=_('Notification of file processing by Cart@DS CS'),
parameters={
'signed_dossier_id': {
'description': _('Signed identifier of file')
},
})
def notification(self, request, signed_dossier_id):
signer = Signer(salt='cart@ds_cs/dossier')
dossier_id = signer.unsign(signed_dossier_id)
dossier = CartaDSDossier.objects.get(id=dossier_id)
dossier.notification_message = request.body
notification = etree.fromstring(request.POST['notification'])
dossier.cartads_id_dossier = notification.find('InformationsComplementaires/IdDossierCartads').text
dossier.cartads_numero_dossier = notification.find('InformationsComplementaires/NumeroDossier').text
dossier.save()
return HttpResponse('ok', content_type='text/plain')
@endpoint(perm='can_access',
description=_('Get status of file'),
parameters={
'dossier_id': {
'description': _('Identifier of file'),
}
})
def status(self, request, dossier_id):
dossier = CartaDSDossier.objects.get(id=dossier_id)
extra = None
if dossier.cartads_id_dossier:
client = self.soap_client(wsdl_url=self.get_wsdl_url('ServiceEtapeDossier'))
resp = client.service.GetEtapesDossier(self.get_token(),
dossier.cartads_id_dossier, [])
steps = []
for step in resp:
steps.append(step)
steps.sort(key=lambda x: x['DateReference'])
status_id = 'cartads-%s' % steps[-1]['IdEtape']
status_label = steps[-1]['LibelleEtape']
extra = {}
for key in steps[-1]:
extra[key] = steps[-1][key]
elif dossier.zip_sent:
status_id = 'zip-sent'
status_label = _('File sent')
elif dossier.zip_ready:
status_id = _('File ready to be sent')
else:
status_label = 'pending'
return {'status_id': status_id, 'status_label': status_label, 'extra': extra}