446 lines
18 KiB
Python
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}
|