passerelle/passerelle/apps/mdel/models.py

409 lines
15 KiB
Python

# -*- coding: utf-8 -*-
# Passerelle - uniform access to data and services
# Copyright (C) 2016 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 os
from django.db import models
from django.utils import six
from django.utils.translation import ugettext_lazy as _
from passerelle.base.models import BaseResource
from passerelle.compat import json_loads
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError
from . import mdel
from .utils import zipdir, get_file_content_from_zip, parse_date
DEMAND_TYPES = ['ILE-LA', 'RCO-LA', 'AEC-LA']
STATUS_MAPPING = {
'100': 'closed',
'20': 'rejected',
'19': 'accepted',
'17': 'information needed',
'16': 'in progress',
'15': 'invalid',
'14': 'imported'
}
APPLICANTS = [
{"id": "PersonneConcernee", "text": "La personne concernée par l'acte"},
{"id": "PereMere", "text": "Son père ou sa mère"},
{"id": "Conjoint", "text": "Son conjoint ou sa conjointe"},
{"id": "Fils", "text": "Son fils ou sa fille"},
{"id": "GrandPere", "text": "Son grand-père ou sa grand-mère"},
{"id": "PetitFils", "text": "Son petit-fils ou sa petite-fille"},
{"id": "Representant", "text": "Son représentant légal"},
{"id": "Heriter", "text": "Son héritier"},
{"id": "Autre", "text": "Autre"}]
CERTIFICATES = [
{'id': 'NAISSANCE', 'text': 'Acte de naissance'},
{'id': 'MARIAGE', 'text': 'Acte de mariage'},
{'id': 'DECES', 'text': 'Acte de décès'}]
CERTIFICATE_TYPES = [
{'id': 'COPIE-INTEGRALE', 'text': 'Copie intégrale'},
{'id': 'EXTRAIT-AVEC-FILIATION', 'text': 'Extrait avec filiation'},
{'id': 'EXTRAIT-SANS-FILIATION', 'text': 'Extrait sans filiation'},
{'id': 'EXTRAIT-PLURILINGUE', 'text': 'Extrait plurilingue'}]
class MDEL(BaseResource):
"""Connector converting Publik's Demand into
MDEL (XML based format) Demand.
It's able to manage demand such as :
- Inscription sur Liste Eletoral (ILE-LA)
- Acte d'Etat Civil (AEC-LA)
- Recensement Citoyen Obligatoire (RCO-LA)
"""
category = _('Civil Status Connectors')
class Meta:
verbose_name = 'Mes Demarches En Ligne'
@classmethod
def get_verbose_name(cls):
return cls._meta.verbose_name
@endpoint(perm='can_access', methods=['post'])
def create(self, request, *args, **kwargs):
"""Create a demand
"""
formdata = json_loads(request.body)
extra = formdata.pop('extra', {})
fields = formdata.pop('fields', {})
def flatten_payload(*subs):
result = {}
for sub in subs:
result.update(sub) # priority on last sub dict
return result
# flatten the payload
formdata.update(flatten_payload(fields, extra))
demand_type = formdata.pop('demand_type', '').upper()
if demand_type not in DEMAND_TYPES:
raise APIError('demand_type must be : %r' % DEMAND_TYPES)
if 'display_id' not in formdata:
raise APIError('display_id is required')
if 'code_insee' not in formdata:
raise APIError('code_insee is required')
demand_id = formdata.pop('display_id')
demand, created = Demand.objects.get_or_create(num=demand_id, flow_type=demand_type, resource=self)
if not created:
demand.step += 1
demand.create_zip(formdata)
demand.save()
return {'data': {'demand_id': demand.demand_id}}
@endpoint(perm='can_access')
def status(self, request, *args, **kwargs):
"""Return demand's statutes
"""
demand_id = request.GET.get('demand_id', None)
if not demand_id:
raise APIError('demand_id is required')
demand = Demand.objects.get(demand_id=demand_id, resource=self)
status = demand.get_status()
demand.save()
return {'data': status}
@endpoint(perm='can_access')
def applicants(self, request, without=''):
return {'data': [item for item in APPLICANTS
if item.get('id') not in without.split(',')]}
@endpoint(perm='can_access')
def certificates(self, request):
return {'data': CERTIFICATES}
@endpoint(name='certificate-types', perm='can_access')
def certificate_types(self, request, without=''):
return {'data': [item for item in CERTIFICATE_TYPES
if item.get('id') not in without.split(',')]}
@six.python_2_unicode_compatible
class Demand(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
resource = models.ForeignKey(MDEL, on_delete=models.CASCADE)
num = models.CharField(
max_length=64, null=False, primary_key=True, unique=True)
flow_type = models.CharField(max_length=32, null=False)
status = models.CharField(max_length=32, null=True)
step = models.IntegerField(default=0)
demand_id = models.CharField(max_length=128, null=True)
def __str__(self):
return '%s - %s - %s' % (self.resource.slug, self.demand_id, self.status)
class Meta:
unique_together = (('num', 'flow_type'))
def save(self, *args, **kwargs):
self.demand_id = '%s-%s' % (self.num, self.flow_type)
super(Demand, self).save(*args, **kwargs)
@property
def name(self):
if self.flow_type == 'AEC-LA':
return '%s-%s-%d' % (self.num, 'EtatCivil', self.step)
return '%s-%s--%d' % (self.num, self.flow_type, self.step)
@property
def filename(self):
return '%s.zip' % self.name
def create_zip(self, formdata):
"""
Creates demand as zip folder
"""
code_insee = formdata['code_insee']
flow_type = self.flow_type
demand_num = self.num
resource_base_dir = mdel.get_resource_base_dir()
inputs_dir = os.path.join(resource_base_dir, self.resource.slug,
'inputs', self.name)
input_files = {}
attached_files = []
if flow_type == 'ILE-LA':
proofs = [('JI', 'justificatif_identite'), ('JD', 'justificatif_domicile')]
for proof_code, proof_attribute in proofs:
documents = [value for key, value in formdata.items()
if key.startswith(proof_attribute) and isinstance(value, dict) and 'filename' in value and 'content' in value]
if not documents:
raise APIError('%s and all its attributes are required' % proof_attribute)
for document in documents:
filename, b64_content = document.get('filename'), document.get('content')
attached_files.append(mdel.AttachedFile(proof_code, filename, b64_content))
# process address additional information
adresse_complement = []
complement_keys = sorted([key for key in formdata if key.startswith('adresse_complement')])
for key in complement_keys:
adresse_complement.append(formdata[key])
if adresse_complement:
formdata['adresse_complement'] = ', '.join(adresse_complement)
# get contact info
contacts = [('TEL', 'contact_telephone'), ('EMAIL', 'contact_email')]
for contact_code, contact_uri in contacts:
uri = formdata.get(contact_uri)
if uri:
formdata['contact_uri'] = uri
formdata['contact_code'] = contact_code
# mdel protocol only supports two values here (prem & cci)
# but we may want the form to have more specific values;
# we name them (prem|cci)_suffix and squeeze the suffix here.
if not formdata.get('anterieur_situation_raw', None):
raise APIError('anterieur_situation_raw is required')
formdata['anterieur_situation_raw'] = formdata.get('anterieur_situation_raw').split('_')[0]
doc = mdel.ILEData(self.demand_id, formdata)
doc.save(inputs_dir)
input_files['demande'] = doc.filename
for attached_file in attached_files:
attached_file.save(inputs_dir)
elif flow_type == 'RCO-LA':
raise APIError('RCO-LA processing not implemented')
else:
# Set date format
if formdata.get('aec_type_raw') != 'NAISSANCE' and not formdata.get('date_acte'):
raise APIError('<date_acte> is required')
if formdata.get('date_acte'):
formdata['date_acte'] = parse_date(formdata['date_acte'])
if formdata.get('titulaire_naiss_date'):
formdata['titulaire_naiss_date'] = parse_date(formdata['titulaire_naiss_date'])
if formdata.get('titulaire2_naiss_date'):
formdata['titulaire2_naiss_date'] = parse_date(formdata['titulaire2_naiss_date'])
# Ensuring that all titles are uppercase
for key in formdata.keys():
if 'civilite' in key:
formdata[key] = formdata[key].upper()
# Merging street number and street name
demandeur_adresse_voie = formdata.get('demandeur_adresse_voie')
demandeur_adresse_num = formdata.get('demandeur_adresse_num')
if demandeur_adresse_voie and demandeur_adresse_num:
formdata['demandeur_adresse_voie'] = '%s %s' % (demandeur_adresse_num, demandeur_adresse_voie)
# Set foreign address if country is not France
adresse_keys = ['etage', 'batiment', 'voie', 'code_postal', 'ville']
adresse_keys = ['demandeur_adresse_%s' % key for key in adresse_keys]
demandeur_adresse_pays_raw = formdata.get('demandeur_adresse_pays_raw')
demandeur_adresse_etrangere = formdata.get('demandeur_adresse_etrangere')
demandeur_adresse_etrangere_pays_raw = formdata.get('demandeur_adresse_etrangere_pays_raw')
if (demandeur_adresse_etrangere_pays_raw
or (demandeur_adresse_pays_raw and demandeur_adresse_pays_raw != 'FRA')):
formdata.pop('demandeur_adresse_pays_raw', None)
if not demandeur_adresse_etrangere_pays_raw:
formdata['demandeur_adresse_etrangere_pays_raw'] = demandeur_adresse_pays_raw
if demandeur_adresse_etrangere:
# dismiss french address if the foreign one is filled
for key in adresse_keys:
if key in formdata:
del formdata[key]
else:
# build foreign address from french address fields
adresse_etrangere = []
for key in adresse_keys:
value = formdata.pop(key, '')
if value:
if key != 'demandeur_adresse_ville':
adresse_etrangere.append(value)
else:
adresse_etrangere[-1] += ' %s' % value
formdata['demandeur_adresse_etrangere'] = ', '.join(adresse_etrangere)
# Set aec_nature if aec_type_raw == DECES
if formdata.get('aec_type_raw') == 'DECES' and not formdata.get('aec_nature_raw'):
formdata['aec_nature'] = u'Copie integrale'
formdata['aec_nature_raw'] = 'COPIE-INTEGRALE'
# Set motif_demand if none
if not formdata.get('motif_demande'):
formdata['motif_demande'] = 'Autre'
formdata['motif_demande_raw'] = 'Autre'
# handling requester when 'Autre'
if formdata.get('qualite_demandeur_autre') and not formdata.get('qualite_demandeur_raw'):
formdata['qualite_demandeur'] = formdata['qualite_demandeur_autre']
formdata['qualite_demandeur_raw'] = 'Autre'
doc = mdel.AECData(self.demand_id, formdata, self.step)
doc.save(inputs_dir)
input_files['demande'] = doc.filename
submission_date = formdata.get('receipt_time', None)
message = mdel.Message(
flow_type, demand_num, code_insee, date=submission_date,
doc=doc
)
message.save(inputs_dir)
input_files['message'] = message.filename
description = mdel.Description(flow_type, demand_num, code_insee, date=submission_date,
attached_files=attached_files,
step=self.step, doc=doc)
description.save(inputs_dir)
input_files['enveloppe'] = description.filename
# create zip file
return zipdir(inputs_dir, input_files)
def get_status(self):
"""Read demand' statuses from file
"""
result = {
'closed': False,
'status': None,
'comment': ''
}
namespace = {'ns2': 'http://finances.gouv.fr/dgme/pec/message/v1'}
resource_base_dir = mdel.get_resource_base_dir()
output_dir = os.path.join(resource_base_dir, self.resource.slug, 'outputs')
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# list all demand response zip files
zip_files = {}
for zfile in os.listdir(output_dir):
if zfile.lower().startswith(self.demand_id.lower()) and zfile.lower().endswith('.zip'):
fname = os.path.splitext(zfile)[0]
try:
step = int(fname.split('--')[-1])
except (TypeError, ValueError):
continue
zip_files[step] = zfile
# get the file with the highest number
if zip_files:
self.step = max(zip_files)
zip_file = zip_files[self.step]
else:
return result
path = os.path.join(output_dir, zip_file)
content = get_file_content_from_zip(path, 'message.xml')
element = mdel.etree.fromstring(content)
majs = element.findall('ns2:Body/*/*/*/ns2:Maj', namespace)
statuses = []
for maj in majs:
etat = maj.findtext('ns2:Etat', namespaces=namespace)
commentaire = maj.findtext('ns2:Commentaire', namespaces=namespace)
if etat:
statuses.append({
'etat': etat,
'commentaire': commentaire
})
statuses = sorted(statuses, key=lambda x: int(x['etat']))[-2:]
for status in statuses:
if status['etat'] == '100':
result['closed'] = True
else:
result['status'] = STATUS_MAPPING[status['etat']]
result['comment'] = status.get('commentaire', '')
self.status = result['status']
self.save()
return result