409 lines
15 KiB
Python
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
|