0010-initialize-sp_fr-connector-31595.patch
passerelle/apps/sp_fr/DOC.XSD | ||
---|---|---|
1 |
<?xml version="1.0" encoding="UTF-8"?> |
|
2 |
<!-- edited with XMLSpy v2010 (http://www.altova.com) by BULL SAS (BULL SAS) --> |
|
3 |
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified"> |
|
4 |
<xs:complexType name="DECLARANT"> |
|
5 |
<xs:sequence> |
|
6 |
<xs:element name="identité" type="identité" minOccurs="0"/> |
|
7 |
<xs:element name="designation-permis" type="designation-permis" minOccurs="0"/> |
|
8 |
<xs:element name="coordonnees" type="coordonnees" minOccurs="0"/> |
|
9 |
</xs:sequence> |
|
10 |
</xs:complexType> |
|
11 |
<xs:complexType name="identité"> |
|
12 |
<xs:sequence> |
|
13 |
<xs:element name="type-personne" type="xs:boolean" minOccurs="0"/> |
|
14 |
<xs:element name="personne-physique" type="personne-physique" minOccurs="0"/> |
|
15 |
<xs:element name="personne-morale" type="personne-morale" minOccurs="0"/> |
|
16 |
</xs:sequence> |
|
17 |
</xs:complexType> |
|
18 |
<xs:complexType name="personne-physique"> |
|
19 |
<xs:sequence> |
|
20 |
<xs:element name="civilité" type="xs:string" minOccurs="0"/> |
|
21 |
<xs:element name="nom" type="xs:string" minOccurs="0"/> |
|
22 |
<xs:element name="prenom" type="xs:string" minOccurs="0"/> |
|
23 |
</xs:sequence> |
|
24 |
</xs:complexType> |
|
25 |
<xs:complexType name="personne-morale"> |
|
26 |
<xs:sequence> |
|
27 |
<xs:element name="denomination"/> |
|
28 |
<xs:element name="raison-sociale"/> |
|
29 |
<xs:element name="SIRET"/> |
|
30 |
<xs:element name="categorie-juridique"/> |
|
31 |
<xs:element name="representant-personne-morale" type="representant-personne-morale"/> |
|
32 |
</xs:sequence> |
|
33 |
</xs:complexType> |
|
34 |
<xs:complexType name="representant-personne-morale"> |
|
35 |
<xs:sequence> |
|
36 |
<xs:element name="civilité" type="xs:string" minOccurs="0"/> |
|
37 |
<xs:element name="nom" type="xs:string" minOccurs="0"/> |
|
38 |
<xs:element name="prenom" type="xs:string" minOccurs="0"/> |
|
39 |
</xs:sequence> |
|
40 |
</xs:complexType> |
|
41 |
<xs:element name="DOC"> |
|
42 |
<xs:complexType> |
|
43 |
<xs:sequence> |
|
44 |
<xs:element name="DECLARANT" type="DECLARANT" minOccurs="0"/> |
|
45 |
<xs:element name="OUVERTURE-CHANTIER" type="OUVERTURE-CHANTIER" minOccurs="0"/> |
|
46 |
<xs:element name="acceptation" type="xs:boolean" minOccurs="0"/> |
|
47 |
</xs:sequence> |
|
48 |
</xs:complexType> |
|
49 |
</xs:element> |
|
50 |
<xs:complexType name="designation-permis"> |
|
51 |
<xs:sequence> |
|
52 |
<xs:element name="numero-permis_construire" type="xs:string" minOccurs="0"/> |
|
53 |
<xs:element name="numero-permis_amenager" type="xs:string" minOccurs="0"/> |
|
54 |
</xs:sequence> |
|
55 |
</xs:complexType> |
|
56 |
<xs:complexType name="coordonnees"> |
|
57 |
<xs:sequence> |
|
58 |
<xs:element name="adresse" type="adresse" minOccurs="0"/> |
|
59 |
<xs:element name="courriel" type="xs:string" minOccurs="0"/> |
|
60 |
</xs:sequence> |
|
61 |
</xs:complexType> |
|
62 |
<xs:complexType name="adresse"> |
|
63 |
<xs:sequence> |
|
64 |
<xs:element name="numero-voie" type="xs:string" minOccurs="0"/> |
|
65 |
<xs:element name="extension" type="xs:string" minOccurs="0"/> |
|
66 |
<xs:element name="type-voie" type="xs:string" minOccurs="0"/> |
|
67 |
<xs:element name="nom-voie" type="xs:string" minOccurs="0"/> |
|
68 |
<xs:element name="lieu-dit" type="xs:string" minOccurs="0"/> |
|
69 |
<xs:element name="boite-postale" type="xs:string" minOccurs="0"/> |
|
70 |
<xs:element name="code-postal" type="xs:string" minOccurs="0"/> |
|
71 |
<xs:element name="localite" type="xs:string" minOccurs="0"/> |
|
72 |
<xs:element name="bureau-cedex" type="xs:string" minOccurs="0"/> |
|
73 |
<xs:element name="pays" type="xs:string" minOccurs="0"/> |
|
74 |
<xs:element name="division-territoriale" type="xs:string" minOccurs="0"/> |
|
75 |
</xs:sequence> |
|
76 |
</xs:complexType> |
|
77 |
<xs:complexType name="OUVERTURE-CHANTIER"> |
|
78 |
<xs:sequence> |
|
79 |
<xs:element name="date-ouverture" type="xs:date" minOccurs="0"/> |
|
80 |
<xs:element name="totalite-travaux" type="xs:boolean" minOccurs="0"/> |
|
81 |
<xs:element name="tranche-travaux" type="tranche-travaux" minOccurs="0"/> |
|
82 |
<xs:element name="autorisation-differer-travaux" type="xs:string" minOccurs="0"/> |
|
83 |
<xs:element name="SHON" type="xs:string" minOccurs="0"/> |
|
84 |
<xs:element name="nombre-logements-commences" type="nombre-logements-commences" minOccurs="0"/> |
|
85 |
<xs:element name="repartition-type-financement" type="repartition-type-financement" minOccurs="0"/> |
|
86 |
</xs:sequence> |
|
87 |
</xs:complexType> |
|
88 |
<xs:complexType name="tranche-travaux"> |
|
89 |
<xs:sequence> |
|
90 |
<xs:element name="amenagements-commences" type="xs:string" minOccurs="0"/> |
|
91 |
</xs:sequence> |
|
92 |
</xs:complexType> |
|
93 |
<xs:complexType name="nombre-logements-commences"> |
|
94 |
<xs:sequence> |
|
95 |
<xs:element name="total-logements" type="xs:int" minOccurs="0"/> |
|
96 |
<xs:element name="individuels" type="xs:int" minOccurs="0"/> |
|
97 |
<xs:element name="collectifs" type="xs:int" minOccurs="0"/> |
|
98 |
</xs:sequence> |
|
99 |
</xs:complexType> |
|
100 |
<xs:complexType name="repartition-type-financement"> |
|
101 |
<xs:sequence> |
|
102 |
<xs:element name="logement-locatif-social" type="xs:int" minOccurs="0"/> |
|
103 |
<xs:element name="accession-aidee" type="xs:int" minOccurs="0"/> |
|
104 |
<xs:element name="pret-taux-zero" type="xs:int" minOccurs="0"/> |
|
105 |
<xs:element name="autres-financements" type="xs:int" minOccurs="0"/> |
|
106 |
</xs:sequence> |
|
107 |
</xs:complexType> |
|
108 |
</xs:schema> |
passerelle/apps/sp_fr/RCO.XSD | ||
---|---|---|
1 |
<?xml version="1.0" encoding="UTF-8"?> |
|
2 |
<!-- edited with XMLSpy v2010 rel. 2 (http://www.altova.com) by BULL SAS (BULL SAS) --> |
|
3 |
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|
4 |
<xs:element name="RecensementCitoyen"> |
|
5 |
<xs:complexType> |
|
6 |
<xs:sequence> |
|
7 |
<xs:element name="Convention" type="xs:string"/> |
|
8 |
<xs:element name="Formalite"> |
|
9 |
<xs:complexType> |
|
10 |
<xs:sequence> |
|
11 |
<xs:element name="Identifiant" type="xs:string"/> |
|
12 |
<xs:element name="FormaliteType" type="xs:string"/> |
|
13 |
<xs:element name="DateSoumission" type="xs:string"/> |
|
14 |
<xs:element name="FormaliteMotifCode" type="xs:string" maxOccurs="2"/> |
|
15 |
<xs:element name="FormaliteModeCode" type="xs:string"/> |
|
16 |
</xs:sequence> |
|
17 |
</xs:complexType> |
|
18 |
</xs:element> |
|
19 |
<xs:element name="Personne"> |
|
20 |
<xs:complexType> |
|
21 |
<xs:sequence> |
|
22 |
<xs:element name="Civilite" type="xs:string"/> |
|
23 |
<xs:element name="Sexe" type="xs:string"/> |
|
24 |
<xs:element name="NomFamille" type="xs:string"/> |
|
25 |
<xs:element name="NomUsage" type="xs:string"/> |
|
26 |
<xs:element name="PrenomUsuel" type="xs:string"/> |
|
27 |
<xs:element name="Prenom" type="xs:string" maxOccurs="2"/> |
|
28 |
<xs:element name="DateNaissance" type="xs:string"/> |
|
29 |
<xs:element name="PaysNaissance" type="xs:string"/> |
|
30 |
<xs:element name="Nationalite" type="xs:string"/> |
|
31 |
<xs:element name="CodeINSEENaissance" type="xs:string"/> |
|
32 |
<xs:element name="LieuNaissance" maxOccurs="2"> |
|
33 |
<xs:complexType> |
|
34 |
<xs:sequence> |
|
35 |
<xs:element name="Code" type="xs:string"/> |
|
36 |
<xs:element name="Nom" type="xs:string"/> |
|
37 |
</xs:sequence> |
|
38 |
</xs:complexType> |
|
39 |
</xs:element> |
|
40 |
<xs:element name="AdresseDomicile"> |
|
41 |
<xs:complexType> |
|
42 |
<xs:sequence> |
|
43 |
<xs:element name="PointDeRemise" type="xs:string"/> |
|
44 |
<xs:element name="Complement" type="xs:string"/> |
|
45 |
<xs:element name="NumeroVoie" type="xs:string"/> |
|
46 |
<xs:element name="Extension" type="xs:string"/> |
|
47 |
<xs:element name="TypeVoie" type="xs:string"/> |
|
48 |
<xs:element name="NomVoie" type="xs:string"/> |
|
49 |
<xs:element name="LieuDit" type="xs:string"/> |
|
50 |
<xs:element name="CodePostal" type="xs:string"/> |
|
51 |
<xs:element name="Localite" type="xs:string"/> |
|
52 |
<xs:element name="CodeINSEE" type="xs:string"/> |
|
53 |
</xs:sequence> |
|
54 |
</xs:complexType> |
|
55 |
</xs:element> |
|
56 |
<xs:element name="AdresseResidence"> |
|
57 |
<xs:complexType> |
|
58 |
<xs:sequence> |
|
59 |
<xs:element name="PointDeRemise" type="xs:string"/> |
|
60 |
<xs:element name="Complement" type="xs:string"/> |
|
61 |
<xs:element name="NumeroVoie" type="xs:string"/> |
|
62 |
<xs:element name="Extension" type="xs:string"/> |
|
63 |
<xs:element name="TypeVoie" type="xs:string"/> |
|
64 |
<xs:element name="NomVoie" type="xs:string"/> |
|
65 |
<xs:element name="LieuDit" type="xs:string"/> |
|
66 |
<xs:element name="CodePostal" type="xs:string"/> |
|
67 |
<xs:element name="Localite" type="xs:string"/> |
|
68 |
</xs:sequence> |
|
69 |
</xs:complexType> |
|
70 |
</xs:element> |
|
71 |
<xs:element name="SituationFamille"> |
|
72 |
<xs:complexType> |
|
73 |
<xs:sequence> |
|
74 |
<xs:element name="SituationMatrimoniale" type="xs:string"/> |
|
75 |
<xs:element name="NombreEnfants" type="xs:string"/> |
|
76 |
<xs:element name="Pupille" type="xs:string"/> |
|
77 |
<xs:element name="NombreFrereSoeur" type="xs:string"/> |
|
78 |
</xs:sequence> |
|
79 |
</xs:complexType> |
|
80 |
</xs:element> |
|
81 |
<xs:element name="MethodeContact" maxOccurs="2"> |
|
82 |
<xs:complexType> |
|
83 |
<xs:sequence> |
|
84 |
<xs:element name="URI" type="xs:string"/> |
|
85 |
<xs:element name="CanalCode" type="xs:string"/> |
|
86 |
</xs:sequence> |
|
87 |
</xs:complexType> |
|
88 |
</xs:element> |
|
89 |
</xs:sequence> |
|
90 |
</xs:complexType> |
|
91 |
</xs:element> |
|
92 |
<xs:element name="FiliationPere"> |
|
93 |
<xs:complexType> |
|
94 |
<xs:sequence> |
|
95 |
<xs:element name="NomFamille" type="xs:string"/> |
|
96 |
<xs:element name="PrenomUsuel" type="xs:string"/> |
|
97 |
<xs:element name="Prenom" type="xs:string" maxOccurs="2"/> |
|
98 |
<xs:element name="DateNaissance" type="xs:string"/> |
|
99 |
<xs:element name="PaysNaissance" type="xs:string"/> |
|
100 |
<xs:element name="Nationalite" type="xs:string"/> |
|
101 |
<xs:element name="CodeINSEENaissance" type="xs:string"/> |
|
102 |
<xs:element name="LieuNaissance" maxOccurs="2"> |
|
103 |
<xs:complexType> |
|
104 |
<xs:sequence> |
|
105 |
<xs:element name="Code" type="xs:string"/> |
|
106 |
<xs:element name="Nom" type="xs:string"/> |
|
107 |
</xs:sequence> |
|
108 |
</xs:complexType> |
|
109 |
</xs:element> |
|
110 |
</xs:sequence> |
|
111 |
</xs:complexType> |
|
112 |
</xs:element> |
|
113 |
<xs:element name="FiliationMere"> |
|
114 |
<xs:complexType> |
|
115 |
<xs:sequence> |
|
116 |
<xs:element name="NomFamille" type="xs:string"/> |
|
117 |
<xs:element name="PrenomUsuel" type="xs:string"/> |
|
118 |
<xs:element name="Prenom" type="xs:string" maxOccurs="2"/> |
|
119 |
<xs:element name="DateNaissance" type="xs:string"/> |
|
120 |
<xs:element name="PaysNaissance" type="xs:string"/> |
|
121 |
<xs:element name="Nationalite" type="xs:string"/> |
|
122 |
<xs:element name="CodeINSEENaissance" type="xs:string"/> |
|
123 |
<xs:element name="LieuNaissance" maxOccurs="2"> |
|
124 |
<xs:complexType> |
|
125 |
<xs:sequence> |
|
126 |
<xs:element name="Code" type="xs:string"/> |
|
127 |
<xs:element name="Nom" type="xs:string"/> |
|
128 |
</xs:sequence> |
|
129 |
</xs:complexType> |
|
130 |
</xs:element> |
|
131 |
</xs:sequence> |
|
132 |
</xs:complexType> |
|
133 |
</xs:element> |
|
134 |
</xs:sequence> |
|
135 |
</xs:complexType> |
|
136 |
</xs:element> |
|
137 |
</xs:schema> |
passerelle/apps/sp_fr/forms.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django import forms |
|
18 | ||
19 |
from . import models, fields |
|
20 | ||
21 | ||
22 |
class MappingForm(forms.ModelForm): |
|
23 | ||
24 |
def __init__(self, *args, **kwargs): |
|
25 |
super(MappingForm, self).__init__(*args, **kwargs) |
|
26 |
if self.instance.procedure and self.instance and self.instance.formdef: |
|
27 |
choices = [('', '--------')] + [(v, v) for v in self.instance.variables] |
|
28 |
for i, field in enumerate(self.schema_fields()): |
|
29 |
label = field.label |
|
30 |
label += ' (%s)' % (field.varname or 'NO VARNAME') |
|
31 |
base_name = str(field.varname or i) |
|
32 |
initial = self.instance.rules.get('fields', {}).get(base_name) |
|
33 |
self.fields['field_%s' % base_name] = fields.VariableAndExpressionField( |
|
34 |
label=label, |
|
35 |
choices=choices, |
|
36 |
initial=initial, |
|
37 |
required=False) |
|
38 | ||
39 |
def table_fields(self): |
|
40 |
return [field for field in self if field.name.startswith('field_')] |
|
41 | ||
42 |
def schema_fields(self): |
|
43 |
if self.instance and self.instance.formdef: |
|
44 |
schema = self.instance.formdef.schema |
|
45 |
for i, field in enumerate(schema.fields): |
|
46 |
if field.type in ('page', 'comment', 'title', 'subtitle'): |
|
47 |
continue |
|
48 |
yield field |
|
49 | ||
50 |
def save(self, commit=True): |
|
51 |
fields = {} |
|
52 |
for key in self.cleaned_data: |
|
53 |
if not key.startswith('field_'): |
|
54 |
continue |
|
55 |
if not self.cleaned_data[key]: |
|
56 |
continue |
|
57 |
real_key = key[len('field_'):] |
|
58 |
value = self.cleaned_data[key].copy() |
|
59 |
value['label'] = self.fields[key].label |
|
60 |
fields[real_key] = value |
|
61 |
self.instance.rules['fields'] = fields |
|
62 |
return super(MappingForm, self).save(commit=commit) |
|
63 | ||
64 |
class Meta: |
|
65 |
model = models.Mapping |
|
66 |
fields = [ |
|
67 |
'procedure', |
|
68 |
'formdef', |
|
69 |
] |
passerelle/apps/sp_fr/models.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# passerelle - uniform access to multiple data sources and services |
|
3 |
# Copyright (C) 2019 Entr'ouvert |
|
4 |
# |
|
5 |
# This program is free software: you can redistribute it and/or modify it |
|
6 |
# under the terms of the GNU Affero General Public License as published |
|
7 |
# by the Free Software Foundation, either version 3 of the License, or |
|
8 |
# (at your option) any later version. |
|
9 |
# |
|
10 |
# This program is distributed in the hope that it will be useful, |
|
11 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 |
# GNU Affero General Public License for more details. |
|
14 |
# |
|
15 |
# You should have received a copy of the GNU Affero General Public License |
|
16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
17 | ||
18 |
import re |
|
19 |
import os |
|
20 |
import stat |
|
21 |
import zipfile |
|
22 |
import collections |
|
23 |
import io |
|
24 |
import base64 |
|
25 |
import datetime |
|
26 | ||
27 |
from lxml import etree as ET |
|
28 | ||
29 |
from django.core.urlresolvers import reverse |
|
30 |
from django.db import models, transaction |
|
31 |
from django.template import engines |
|
32 |
from django.utils.translation import ugettext_lazy as _ |
|
33 | ||
34 |
from jsonfield import JSONField |
|
35 | ||
36 |
from passerelle.base.models import BaseResource |
|
37 |
from passerelle.utils.api import endpoint |
|
38 |
from passerelle.utils.sftp import SFTPField |
|
39 |
from passerelle.utils.wcs import FormDefField, get_wcs_choices |
|
40 |
from passerelle.utils.xsd import Schema |
|
41 |
from passerelle.utils.xml import text_content |
|
42 | ||
43 | ||
44 |
PROCEDURE_DOC = 'DOC' |
|
45 |
PROCEDURE_RCO = 'RCO' |
|
46 |
PROCEDURE_DDPACS = 'DDPACS' |
|
47 |
PROCEDURES = [ |
|
48 |
(PROCEDURE_DOC, _('Request for construction site opening')), |
|
49 |
(PROCEDURE_RCO, _('Request for mandatory citizen census')), |
|
50 |
(PROCEDURE_DDPACS, _('Pre-request for citizen solidarity pact')), |
|
51 |
] |
|
52 | ||
53 |
ET.register_namespace('dgme-metier', 'http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier') |
|
54 | ||
55 | ||
56 |
def simplify(s): |
|
57 |
''' |
|
58 |
Simplify a string, trying to transform it to lower ascii chars (a-z, 0-9) |
|
59 |
and minimize spaces. Used to compare strings on ?q=something requests. |
|
60 |
''' |
|
61 |
if not s: |
|
62 |
return '' |
|
63 |
if not isinstance(s, six.text_type): |
|
64 |
s = six.text_type(s, 'utf-8', 'ignore') |
|
65 |
s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') |
|
66 |
s = re.sub(r'[^\w\s\'-_]', '', s) |
|
67 |
s = re.sub(r'[\s\']+', ' ', s) |
|
68 |
return s.strip().lower() |
|
69 | ||
70 | ||
71 |
class Resource(BaseResource): |
|
72 |
category = _('Business Process Connectors') |
|
73 | ||
74 |
input_sftp = SFTPField( |
|
75 |
verbose_name=_('Input SFTP URL'), |
|
76 |
null=True) |
|
77 | ||
78 |
output_sftp = SFTPField( |
|
79 |
verbose_name=_('Output SFTP URL'), |
|
80 |
null=True) |
|
81 | ||
82 |
def check_status(self): |
|
83 |
with self.input_sftp as sftp: |
|
84 |
sftp.listdir() |
|
85 |
with self.output_sftp as sftp: |
|
86 |
sftp.listdir() |
|
87 |
get_wcs_choices(self.requests) |
|
88 | ||
89 |
@endpoint(name='ping', description=_('Check Solis API availability')) |
|
90 |
def ping(self, request): |
|
91 |
self.check_status() |
|
92 |
return {'err': 0} |
|
93 | ||
94 |
def run_loop(self, count=1): |
|
95 |
with transaction.atomic(): |
|
96 |
# lock resource |
|
97 |
r = Resource.objects.select_for_update(skip_locked=True).filter(pk=self.pk) |
|
98 |
if not r: |
|
99 |
# already locked |
|
100 |
self.logger.info('did nothing') |
|
101 |
return |
|
102 |
with self.input_sftp as sftp: |
|
103 |
try: |
|
104 |
sftp.lstat('DONE') |
|
105 |
except IOError: |
|
106 |
sftp.mkdir('DONE') |
|
107 | ||
108 |
def helper(): |
|
109 |
for file_stat in sftp.listdir_attr(): |
|
110 |
if stat.S_ISDIR(file_stat.st_mode): |
|
111 |
continue |
|
112 |
yield file_stat.filename |
|
113 | ||
114 |
mappings = {mapping.procedure: mapping for mapping in self.mappings.all()} |
|
115 | ||
116 |
for filename, i in zip(helper(), range(count)): |
|
117 |
self.handle_filename(filename, sftp, mappings) |
|
118 | ||
119 |
def handle_filename(self, filename, sftp, mappings): |
|
120 |
nsmap = { |
|
121 |
'dgme-metier': 'http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier' |
|
122 |
} |
|
123 |
routage_xpath = ET.XPath( |
|
124 |
('dgme-metier:Routage/dgme-metier:Donnee/dgme-metier:Valeur/text()'), |
|
125 |
namespaces=nsmap) |
|
126 |
m = re.match(r'(.*)-([A-Z]+)-(\d+).zip', filename) |
|
127 |
if not m: |
|
128 |
self.logger.warning('found file with an unknown pattern %s moving in DONE/', filename) |
|
129 |
return |
|
130 |
mdel_identifier, procedure, mdel_sequence = m.groups() |
|
131 |
if procedure not in mappings: |
|
132 |
self.logger.warning('found file for an unsupported procedure %s', procedure) |
|
133 |
return |
|
134 |
mapping = mappings[procedure] |
|
135 |
self.logger.info('found %s %s %s, handling', mdel_identifier, procedure, mdel_sequence) |
|
136 |
with sftp.open(filename) as fd: |
|
137 |
try: |
|
138 |
archive = zipfile.ZipFile(fd) |
|
139 |
except Exception: |
|
140 |
self.logger.error('could not load zipfile %s', filename) |
|
141 |
return |
|
142 |
doc_files = [] |
|
143 |
ent_files = [] |
|
144 |
attachments = {} |
|
145 |
for name in archive.namelist(): |
|
146 |
if re.match(r'^.*-ent-\d+(?:-.*)?.xml$', name): |
|
147 |
ent_files.append(name) |
|
148 |
if re.match(r'^.*-doc-\d+-XML-\d+(?:-.*)?\.xml$', name): |
|
149 |
doc_files.append(name) |
|
150 |
m = re.match(r'^.*-pj-([^-]+)-\d+\.([^.]+)$', name) |
|
151 |
if m: |
|
152 |
attachment_type, extension = m.groups() |
|
153 |
attachments.setdefault(attachment_type, []).append(name) |
|
154 |
if len(ent_files) != 1: |
|
155 |
self.logger.warning('too many/few ent files found: %s', ent_files) |
|
156 |
return |
|
157 |
if len(doc_files) != 1: |
|
158 |
self.logger.warning('too many/few doc files found: %s', doc_files) |
|
159 |
return |
|
160 |
for key in attachments: |
|
161 |
if len(attachments[key]) > 1: |
|
162 |
self.logger.warning('too many attachments of kind %s: %s', key, attachments[key]) |
|
163 |
name = attachments[key][0] |
|
164 |
content = archive.open(attachments[key][0]).read() |
|
165 |
attachments[key] = { |
|
166 |
'filename': name, |
|
167 |
'content': base64.b64encode(content).decode('ascii'), |
|
168 |
'content_type': 'application/octet-stream', |
|
169 |
} |
|
170 |
if procedure == 'RCO' and not attachments: |
|
171 |
self.logger.warning('no attachments but RCO requires them') |
|
172 |
return |
|
173 |
ent_file = ent_files[0] |
|
174 |
doc_file = doc_files[0] |
|
175 | ||
176 |
with archive.open(ent_file) as fd: |
|
177 |
document = ET.parse(fd) |
|
178 |
insee_codes = routage_xpath(document) |
|
179 |
if len(insee_codes) != 1: |
|
180 |
self.logger.warning('too many/few insee codes found: %s', insee_codes) |
|
181 |
return |
|
182 |
insee_code = insee_codes[0] |
|
183 |
data = {'insee_code': insee_code} |
|
184 |
data.update(attachments) |
|
185 |
with archive.open(doc_file) as fd: |
|
186 |
document = ET.parse(fd) |
|
187 |
data.update(self.extract_data(document)) |
|
188 |
if hasattr(self, 'update_data_%s' % procedure): |
|
189 |
getattr(self, 'update_data_%s' % procedure)(mapping, data) |
|
190 | ||
191 |
formdef = mapping.formdef |
|
192 |
formdef.session = self.requests |
|
193 | ||
194 |
with formdef.submit() as submitter: |
|
195 |
submitter.submission_channel = 'web' |
|
196 |
submitter.submission_context = { |
|
197 |
'mdel_procedure': procedure, |
|
198 |
'mdel_identifier': mdel_identifier, |
|
199 |
'mdel_sequence': mdel_sequence, |
|
200 |
} |
|
201 |
for field in mapping.rules.get('fields', {}): |
|
202 |
variable = mapping.rules['fields'][field]['variable'] |
|
203 |
expression = mapping.rules['fields'][field]['expression'] |
|
204 |
value = data.get(variable) |
|
205 |
if expression.strip(): |
|
206 |
template = engines['django'].from_string(expression) |
|
207 |
context = data.copy() |
|
208 |
context['value'] = value |
|
209 |
value = template.render(context) |
|
210 |
submitter.set(field, value) |
|
211 |
import pprint |
|
212 |
print 'Payload' |
|
213 |
pprint.pprint(submitter.payload()) |
|
214 | ||
215 |
for key in sorted(data): |
|
216 |
if not isinstance(data[key], dict): |
|
217 |
print key, data[key] |
|
218 |
else: |
|
219 |
print key, '<fichier>', data[key]['filename'] |
|
220 | ||
221 |
def update_data_DOC(self, mapping, data): |
|
222 |
variables = list(mapping.variables) |
|
223 |
assert all(key in variables for key in data) |
|
224 | ||
225 |
def get(name): |
|
226 |
# prevent error in manual mapping |
|
227 |
if name not in variables: |
|
228 |
print '\n'.join(sorted(variables)) |
|
229 |
assert False, name |
|
230 |
return data.get(name, '') |
|
231 | ||
232 |
numero_permis_construire = get('doc_declarant_designation_permis_numero_permis_construire') |
|
233 |
numero_permis_amenager = get('doc_declarant_designation_permis_numero_permis_amenager') |
|
234 |
data['type_permis'] = u'Un permis de construire' if numero_permis_construire else u'Un permis d\'aménager' |
|
235 |
data['numero_permis'] = numero_permis_construire or numero_permis_amenager |
|
236 |
particulier = get('doc_declarant_identite_type_personne').strip().lower() == 'true' |
|
237 |
data['type_declarant'] = u'Un particulier' if particulier else u'Une personne morale' |
|
238 |
if particulier: |
|
239 |
data['nom'] = get('doc_declarant_identite_personne_physique_nom') |
|
240 |
data['prenoms'] = get('doc_declarant_identite_personne_physique_prenom') |
|
241 |
else: |
|
242 |
data['nom'] = get('doc_declarant_identite_personne_morale_representant_personne_morale_nom') |
|
243 |
data['prenoms'] = get('doc_declarant_identite_personne_morale_representant_personne_morale_prenom') |
|
244 |
mapping = { |
|
245 |
'1000': 'Monsieur', |
|
246 |
'1001': 'Madame', |
|
247 |
'1002': 'Madame et Monsieur', |
|
248 |
} |
|
249 |
if particulier: |
|
250 |
data['civilite_particulier'] = mapping.get(get('doc_declarant_identite_personne_physique_civilite'), '') |
|
251 |
else: |
|
252 |
data['civilite_pm'] = mapping.get( |
|
253 |
get('doc_declarant_identite_personne_morale_representant_personne_morale_civilite'), '') |
|
254 |
data['portee'] = (u'Pour la totalité des travaux' |
|
255 |
if get('doc_ouverture_chantier_totalite_travaux').lower().strip() == 'true' |
|
256 |
else u'Pour une tranche des travaux') |
|
257 |
assert all(key in variables for key in data) |
|
258 | ||
259 |
def update_data_RCO(self, mapping, data): |
|
260 |
variables = list(mapping.variables) |
|
261 |
assert all(key in variables for key in data) |
|
262 | ||
263 |
def get(name): |
|
264 |
# prevent error in manual mapping |
|
265 |
if name not in variables: |
|
266 |
print '\n'.join(sorted(variables)) |
|
267 |
assert False, name |
|
268 |
return data.get(name, '') |
|
269 | ||
270 |
motif = ( |
|
271 |
get('recensementcitoyen_formalite_formalitemotifcode_1') |
|
272 |
or get('recensementcitoyen_formalite_formalitemotifcode_2') |
|
273 |
) |
|
274 |
data['motif'] = { |
|
275 |
'RECENSEMENT': '1', |
|
276 |
'EXEMPTION': '2' |
|
277 |
}[motif] |
|
278 |
if data['motif'] == '2': |
|
279 |
data['motif_exempte'] = ( |
|
280 |
u"Titulaire d'une carte d'invalidité de 80% minimum" |
|
281 |
if get('recensementcitoyen_formalite_formalitemotifcode_2') == 'INFIRME' |
|
282 |
else u"Autre situation") |
|
283 |
data['justificatif_exemption'] = get('je') |
|
284 |
data['double_nationalite'] = ( |
|
285 |
'Oui' |
|
286 |
if get('recensementcitoyen_personne_nationalite') |
|
287 |
else 'Non') |
|
288 |
data['residence_differente'] = ( |
|
289 |
'Oui' |
|
290 |
if get('recensementcitoyen_personne_adresseresidence_localite') |
|
291 |
else 'Non') |
|
292 |
data['civilite'] = ( |
|
293 |
'Monsieur' |
|
294 |
if get('recensementcitoyen_personne_civilite') == 'M' |
|
295 |
else 'Madame' |
|
296 |
) |
|
297 | ||
298 |
def get_lieu_naissance(variable, code): |
|
299 |
for idx in ['', '_1', '_2']: |
|
300 |
v = variable + idx |
|
301 |
if get(v + '_code') == code: |
|
302 |
return get(v + '_nom') |
|
303 | ||
304 |
data['cp_naissance'] = get_lieu_naissance('recensementcitoyen_personne_lieunaissance', 'AUTRE') |
|
305 |
data['commune_naissance'] = get_lieu_naissance('recensementcitoyen_personne_lieunaissance', 'COMMUNE') |
|
306 |
data['justificatif_identite'] = get('ji') |
|
307 |
situation_matrimoniale = get('recensementcitoyen_personne_situationfamille_situationmatrimoniale') |
|
308 |
data['situation_familiale'] = { |
|
309 |
u'Célibataire': u'Célibataire', |
|
310 |
u'Marié': u'Marié(e)', |
|
311 |
}.get(situation_matrimoniale, u'Autres') |
|
312 |
if data['situation_familiale'] == u'Autres': |
|
313 |
data['situation_familiale_precision'] = situation_matrimoniale |
|
314 |
pupille = get('recensementcitoyen_personne_situationfamille_pupille') |
|
315 |
data['pupille'] = ( |
|
316 |
'Oui' |
|
317 |
if pupille |
|
318 |
else 'Non' |
|
319 |
) |
|
320 |
data['pupille_categorie'] = { |
|
321 |
'NATION': u"Pupille de la nation", |
|
322 |
'ETAT': u"Pupille de l'État", |
|
323 |
}.get(pupille) |
|
324 |
for idx in ['', '_1', '_2']: |
|
325 |
code = get('recensementcitoyen_personne_methodecontact%s_canalcode' % idx) |
|
326 |
uri = get('recensementcitoyen_personne_methodecontact%s_uri' % idx) |
|
327 |
if code == 'EMAIL': |
|
328 |
data['courriel'] = uri |
|
329 |
if code == 'TEL': |
|
330 |
data['telephone_fixe'] = uri |
|
331 |
data['justificatif_famille'] = data['jf'] |
|
332 |
data['filiation_inconnue_p1'] = not get('recensementcitoyen_filiationpere_nomfamille') |
|
333 |
data['filiation_inconnue_p2'] = not get('recensementcitoyen_filiationmere_nomfamille') |
|
334 |
data['cp_naissance_p1'] = get_lieu_naissance('recensementcitoyen_filiationpere_lieunaissance', 'AUTRE') |
|
335 |
data['cp_naissance_p2'] = get_lieu_naissance('recensementcitoyen_filiationmere_lieunaissance', 'AUTRE') |
|
336 |
data['commune_naissance_p1'] = get_lieu_naissance('recensementcitoyen_filiationpere_lieunaissance', 'COMMUNE') |
|
337 |
data['commune_naissance_p2'] = get_lieu_naissance('recensementcitoyen_filiationmere_lieunaissance', 'COMMUNE') |
|
338 |
for key in data: |
|
339 |
if key.endswith('_datenaissance') and data[key]: |
|
340 |
data[key] = ( |
|
341 |
datetime.datetime.strptime(data[key], '%d/%m/%Y') |
|
342 |
.date() |
|
343 |
.strftime('%Y-%m-%d') |
|
344 |
) |
|
345 | ||
346 |
def extract_data(self, document): |
|
347 |
root = document.getroot() |
|
348 | ||
349 |
def tag_name(node): |
|
350 |
return simplify(ET.QName(node.tag).localname) |
|
351 | ||
352 |
def helper(path, node): |
|
353 |
if len(node): |
|
354 |
tags = collections.Counter(tag_name(child) for child in node) |
|
355 |
counter = collections.Counter() |
|
356 |
for child in node: |
|
357 |
name = tag_name(child) |
|
358 |
if tags[name] > 1: |
|
359 |
counter[name] += 1 |
|
360 |
name += '_%s' % counter[name] |
|
361 |
for p, value in helper(path + [name], child): |
|
362 |
yield p, value |
|
363 |
else: |
|
364 |
yield path, text_content(node) |
|
365 |
return { |
|
366 |
'_'.join(path).replace('-', '_').replace(' ', ''): value |
|
367 |
for path, value in helper([tag_name(root)], root) |
|
368 |
} |
|
369 | ||
370 |
class Meta: |
|
371 |
verbose_name = _('Service-Public.fr') |
|
372 | ||
373 | ||
374 |
class SPFRMessage(object): |
|
375 |
@classmethod |
|
376 |
def from_file(self, resource, filename, fd): |
|
377 |
m = re.match(r'(.*)-([A-Z]+)-(\d+).zip', filename) |
|
378 |
if not m: |
|
379 |
resource.logger.warning('found file with an unknown pattern %s moving in DONE/', filename) |
|
380 |
return None |
|
381 |
mdel_number, procedure, sequence = m.groups() |
|
382 | ||
383 | ||
384 |
def default_rule(): |
|
385 |
return {} |
|
386 | ||
387 | ||
388 |
class Mapping(models.Model): |
|
389 | ||
390 |
resource = models.ForeignKey( |
|
391 |
Resource, |
|
392 |
verbose_name=_('Resource'), |
|
393 |
related_name='mappings') |
|
394 | ||
395 |
procedure = models.CharField( |
|
396 |
verbose_name=_('Procedure'), |
|
397 |
choices=PROCEDURES, |
|
398 |
unique=True, |
|
399 |
max_length=8) |
|
400 | ||
401 |
formdef = FormDefField( |
|
402 |
verbose_name=_('Formulaire')) |
|
403 | ||
404 |
rules = JSONField( |
|
405 |
verbose_name=_('Rules'), |
|
406 |
default=default_rule) |
|
407 | ||
408 |
def get_absolute_url(self): |
|
409 |
return reverse('sp-fr-mapping-edit', kwargs=dict( |
|
410 |
slug=self.resource.slug, |
|
411 |
pk=self.pk)) |
|
412 | ||
413 |
@property |
|
414 |
def xsd(self): |
|
415 |
doc = ET.parse(os.path.join(os.path.dirname(__file__), '%s.XSD' % self.procedure)) |
|
416 |
schema = Schema() |
|
417 |
schema.visit(doc.getroot()) |
|
418 |
return schema |
|
419 | ||
420 |
@property |
|
421 |
def variables(self): |
|
422 |
yield 'insee_code' |
|
423 |
for path, xsd_type in self.xsd.paths(): |
|
424 |
names = [simplify(tag.localname).replace('-', '_').replace(' ', '') for tag in path] |
|
425 |
yield '_'.join(names) |
|
426 |
if hasattr(self, 'variables_%s' % self.procedure): |
|
427 |
for variable in getattr(self, 'variables_%s' % self.procedure): |
|
428 |
yield variable |
|
429 | ||
430 |
@property |
|
431 |
def variables_DOC(self): |
|
432 |
yield 'type_permis' |
|
433 |
yield 'numero_permis' |
|
434 |
yield 'type_declarant' |
|
435 |
yield 'nom' |
|
436 |
yield 'prenoms' |
|
437 |
yield 'civilite_particulier' |
|
438 |
yield 'civilite_pm' |
|
439 |
yield 'portee' |
|
440 | ||
441 |
@property |
|
442 |
def variables_RCO(self): |
|
443 |
yield 'motif' |
|
444 |
yield 'motif_exemple' |
|
445 |
yield 'justificatif_exemption' |
|
446 |
yield 'double_nationalite' |
|
447 |
yield 'residence_differente' |
|
448 |
yield 'civilite' |
|
449 |
yield 'cp_naissance' |
|
450 |
yield 'commune_naissance' |
|
451 |
yield 'je' |
|
452 |
yield 'ji' |
|
453 |
yield 'situation_familiale' |
|
454 |
yield 'situation_familiale_precision' |
|
455 |
yield 'pupille' |
|
456 |
yield 'pupille_categorie' |
|
457 |
yield 'courriel' |
|
458 |
yield 'telephone_fixe' |
|
459 |
yield 'jf' |
|
460 |
yield 'filiation_inconnue_p1' |
|
461 |
yield 'filiation_inconnue_p2' |
|
462 |
yield 'cp_naissance_p1' |
|
463 |
yield 'cp_naissance_p2' |
|
464 |
yield 'commune_naissance_p1' |
|
465 |
yield 'commune_naissance_p2' |
|
466 | ||
467 |
#def archive_upload_to(instance, filename): |
|
468 |
# return 'sp_fr/{instance.procedure}/{filename}'.format( |
|
469 |
# instance=instance, |
|
470 |
# filename=filename) |
|
471 |
# |
|
472 |
# |
|
473 |
#class Request(models.Model): |
|
474 |
# |
|
475 |
# # To prevent mixing errors from analysing archive from s-p.fr and errors |
|
476 |
# # from pushing to w.c.s we separate processing with three steps: |
|
477 |
# # - receiving, i.e. copying zipfile from SFTP and storing them locally |
|
478 |
# # - processing, i.e. openeing the zipfile and extracting content as we need it |
|
479 |
# # - transferring, pushing content as a new form in w.c.s. |
|
480 |
# STATE_RECEIVED = 'received' |
|
481 |
# STATE_PROCESSED = 'processed' |
|
482 |
# STATE_TRANSFERED = 'transfered' |
|
483 |
# STATE_ERROR = 'error' |
|
484 |
# STATES = [ |
|
485 |
# (STATE_RECEIVED, _('Received')), |
|
486 |
# (STATE_TRANSFERED, _('Transfered')), |
|
487 |
# (STATE_ERROR, _('Transfered')), |
|
488 |
# ] |
|
489 |
# |
|
490 |
# ressource = models.ForeignKey( |
|
491 |
# Resource, |
|
492 |
# verbose_name=_('Resource')) |
|
493 |
# identifier = models.CharField( |
|
494 |
# verbose_name=_('Identifier'), |
|
495 |
# max_length=32) |
|
496 |
# procedure = models.CharField( |
|
497 |
# verbose_name=_('Procedure'), |
|
498 |
# choices=PROCEDURES, |
|
499 |
# max_length=8) |
|
500 |
# sequence_number = models.PositiveIntegerField( |
|
501 |
# verbose_name=_('Sequence number')) |
|
502 |
# |
|
503 |
# state = models.CharField( |
|
504 |
# verbose_name=_('State'), |
|
505 |
# choices=STATES, |
|
506 |
# max_length=16) |
|
507 |
# |
|
508 |
# content = JSONField( |
|
509 |
# verbose_name=_('Content'), |
|
510 |
# null=True) |
|
511 |
# |
|
512 |
# archive = models.FileField( |
|
513 |
# verbose_name=_('Archive'), |
|
514 |
# upload_to=archive_upload_to) |
passerelle/apps/sp_fr/templates/passerelle/widgets/variable_and_expression_widget.html | ||
---|---|---|
1 |
<div class="variable-widget"> |
|
2 |
{% include widget.subwidgets.0.template_name with widget=widget.subwidgets.0 %} |
|
3 |
</div> |
|
4 |
<div class="expression-widget"> |
|
5 |
{% include widget.subwidgets.1.template_name with widget=widget.subwidgets.1 %} |
|
6 |
</div> |
passerelle/apps/sp_fr/templates/sp_fr/mapping_confirm_delete.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/resource_child_confirm_delete.html" %} |
passerelle/apps/sp_fr/templates/sp_fr/mapping_form.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/resource_child_form.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 | ||
5 |
{% block form %} |
|
6 |
{% if form.errors %} |
|
7 |
<div class="errornotice"> |
|
8 |
<p>{% trans "There were errors processing your form." %}</p> |
|
9 |
{% for error in form.non_field_errors %} |
|
10 |
<p>{{ error }}</p> |
|
11 |
{% endfor %} |
|
12 |
{% for field in form %} |
|
13 |
{% if field.is_hidden and field.errors %} |
|
14 |
<p> |
|
15 |
{% for error in field.errors %} |
|
16 |
{% blocktrans with name=field.name %}(Hidden field {{name}}) {{ error }}{% endblocktrans %} |
|
17 |
{% if not forloop.last %}<br>{% endif %} |
|
18 |
{% endfor %} |
|
19 |
</p> |
|
20 |
{% endif %} |
|
21 |
{% endfor %} |
|
22 |
</div> |
|
23 |
{% endif %} |
|
24 |
{% include "gadjo/widget.html" with field=form.procedure %} |
|
25 | ||
26 |
{% include "gadjo/widget.html" with field=form.formdef%} |
|
27 | ||
28 |
<table class="main"> |
|
29 |
<thead> |
|
30 |
<tr> |
|
31 |
<td>Label</td> |
|
32 |
<td>Variable</td> |
|
33 |
</tr> |
|
34 |
</thead> |
|
35 |
<tbody> |
|
36 |
{% for field in form.table_fields %} |
|
37 |
<tr> |
|
38 |
<td>{{ field.label_tag }}</td> |
|
39 |
<td>{{ field }}</td> |
|
40 |
</tr> |
|
41 |
{% endfor %} |
|
42 |
</tbody> |
|
43 |
</table> |
|
44 |
{% endblock %} |
passerelle/apps/sp_fr/templates/sp_fr/resource_detail.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/service_view.html" %} |
|
2 |
{% load i18n passerelle %} |
|
3 | ||
4 |
{% block description %} |
|
5 |
<p> |
|
6 |
{% blocktrans %} |
|
7 |
Connector to forms published by <a href="https://www.service-public.fr/">service-public.fr</a> |
|
8 |
{% endblocktrans %} |
|
9 |
<a href="{% url "sp-fr-run" slug=object.slug %}">Run</a> |
|
10 |
</p> |
|
11 |
{{ block.super }} |
|
12 |
{% endblock %} |
|
13 | ||
14 | ||
15 |
{% block extra-sections %} |
|
16 |
<div id="mappings" class="section"> |
|
17 |
<h3>{% trans "Mappings" %} <a class="button" href="{% url "sp-fr-mapping-new" slug=object.slug %}">{% trans "Add" %}</a></h3> |
|
18 |
<ul> |
|
19 | ||
20 |
{% for mapping in object.mappings.all %} |
|
21 |
<li> |
|
22 |
<fieldset class="gadjo-foldable gadjo-folded" id="sp-fr-mapping-{{ mapping.pk}}"> |
|
23 |
<legend class="gadjo-foldable-widget"> |
|
24 |
<a href="{% url "sp-fr-mapping-edit" connector=object.get_connector_slug slug=object.slug pk=mapping.pk %}">{% blocktrans with procedure=mapping.get_procedure_display formdef=mapping.formdef.title %}From procedure {{ procedure }} to form {{ formdef }}{% endblocktrans %}</a> |
|
25 |
</legend> |
|
26 |
<div class="gadjo-folding"> |
|
27 |
{% for key, value in mapping.rules.fields.items %} |
|
28 |
{% if value %} |
|
29 |
<p>{{ value.label }} : {{ value.variable }} {% if value.expression %}({% trans "with expression" %} <tt>{{ value.expression }}</tt>){% endif %}</p> |
|
30 |
{% endif %} |
|
31 |
{% endfor %} |
|
32 |
<a rel="popup" class="delete" href="{% url "sp-fr-mapping-delete" connector=object.get_connector_slug slug=object.slug pk=mapping.pk %}">{% trans "Delete" %}</a> |
|
33 |
</div> |
|
34 |
</fieldset> |
|
35 |
</li> |
|
36 |
{% endfor %} |
|
37 |
</ul> |
|
38 |
</div> |
|
39 |
{% endblock %} |
passerelle/apps/sp_fr/urls.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.conf.urls import url |
|
18 | ||
19 |
from . import views |
|
20 | ||
21 |
management_urlpatterns = [ |
|
22 |
url(r'^(?P<slug>[\w,-]+)/mapping/new/$', |
|
23 |
views.MappingNew.as_view(), name='sp-fr-mapping-new'), |
|
24 |
url(r'^(?P<slug>[\w,-]+)/mapping/(?P<pk>\d+)/$', |
|
25 |
views.MappingEdit.as_view(), name='sp-fr-mapping-edit'), |
|
26 |
url(r'^(?P<slug>[\w,-]+)/mapping/(?P<pk>\d+)/delete/$', |
|
27 |
views.MappingDelete.as_view(), name='sp-fr-mapping-delete'), |
|
28 |
url(r'^(?P<slug>[\w,-]+)/run/$', |
|
29 |
views.run, name='sp-fr-run'), |
|
30 |
] |
passerelle/apps/sp_fr/views.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.views.generic import UpdateView, CreateView, DeleteView |
|
18 |
from django.shortcuts import get_object_or_404 |
|
19 |
from django.http import HttpResponseRedirect |
|
20 | ||
21 |
from passerelle.base.mixins import ResourceChildViewMixin |
|
22 | ||
23 |
from . import models, forms |
|
24 | ||
25 | ||
26 |
class StayIfChanged(object): |
|
27 |
has_changed = False |
|
28 | ||
29 |
def form_valid(self, form): |
|
30 |
if set(form.changed_data) & set(['procedure', 'formdef']): |
|
31 |
self.has_changed = True |
|
32 |
return super(StayIfChanged, self).form_valid(form) |
|
33 | ||
34 |
def get_success_url(self): |
|
35 |
if self.has_changed: |
|
36 |
return self.get_changed_url() |
|
37 |
return super(StayIfChanged, self).get_success_url() |
|
38 | ||
39 |
def get_changed_url(self): |
|
40 |
return '' |
|
41 | ||
42 | ||
43 |
class MappingNew(StayIfChanged, ResourceChildViewMixin, CreateView): |
|
44 |
model = models.Mapping |
|
45 |
form_class = forms.MappingForm |
|
46 | ||
47 |
def form_valid(self, form): |
|
48 |
form.instance.resource = self.resource |
|
49 |
return super(MappingNew, self).form_valid(form) |
|
50 | ||
51 |
def get_changed_url(self): |
|
52 |
return self.object.get_absolute_url() |
|
53 | ||
54 | ||
55 |
class MappingEdit(StayIfChanged, ResourceChildViewMixin, UpdateView): |
|
56 |
model = models.Mapping |
|
57 |
form_class = forms.MappingForm |
|
58 | ||
59 | ||
60 |
class MappingDelete(ResourceChildViewMixin, DeleteView): |
|
61 |
model = models.Mapping |
|
62 | ||
63 | ||
64 |
def run(request, connector, slug): |
|
65 |
resource = get_object_or_404(models.Resource, slug=slug) |
|
66 |
resource.run_loop(1000) |
|
67 |
return HttpResponseRedirect(resource.get_absolute_url()) |
passerelle/settings.py | ||
---|---|---|
135 | 135 |
'passerelle.apps.feeds', |
136 | 136 |
'passerelle.apps.gdc', |
137 | 137 |
'passerelle.apps.jsondatastore', |
138 |
'passerelle.apps.sp_fr', |
|
138 | 139 |
'passerelle.apps.mobyt', |
139 | 140 |
'passerelle.apps.okina', |
140 | 141 |
'passerelle.apps.opengis', |
passerelle/static/css/style.css | ||
---|---|---|
184 | 184 |
.log-dialog table td { |
185 | 185 |
vertical-align: top; |
186 | 186 |
} |
187 |
.expression-widget input { |
|
188 |
width: 100%; |
|
189 |
} |
|
190 |
.variable-widget select { |
|
191 |
width: 100%; |
|
192 |
} |
tests/test_sp_fr.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# passerelle - uniform access to multiple data sources and services |
|
3 |
# Copyright (C) 2019 Entr'ouvert |
|
4 |
# |
|
5 |
# This program is free software: you can redistribute it and/or modify it |
|
6 |
# under the terms of the GNU Affero General Public License as published |
|
7 |
# by the Free Software Foundation, either version 3 of the License, or |
|
8 |
# (at your option) any later version. |
|
9 |
# |
|
10 |
# This program is distributed in the hope that it will be useful, |
|
11 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 |
# GNU Affero General Public License for more details. |
|
14 |
# |
|
15 |
# You should have received a copy of the GNU Affero General Public License |
|
16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
17 | ||
18 |
import pytest |
|
19 | ||
20 |
from passerelle.apps.sp_fr.models import Resource |
|
21 | ||
22 |
import utils |
|
23 | ||
24 | ||
25 |
DUMMY_CONTENT = { |
|
26 |
'DILA': { |
|
27 |
'a.zip': 'a', |
|
28 |
} |
|
29 |
} |
|
30 | ||
31 | ||
32 |
@pytest.fixture |
|
33 |
def spfr(settings, db, sftpserver): |
|
34 |
settings.KNOWN_SERVICES = { |
|
35 |
'wcs': { |
|
36 |
'eservices': { |
|
37 |
'title': u'Démarches', |
|
38 |
'url': 'https://demarches-hautes-alpes.test.entrouvert.org/', |
|
39 |
'secret': '9a1f62b680c1cabe73cefcbc5ff4cd4f95c24c3be8dd930adb80d8aaa33bfe67', |
|
40 |
'orig': 'passerelle-hautes-alpes.test.entrouvert.org', |
|
41 |
} |
|
42 |
} |
|
43 |
} |
|
44 |
yield utils.make_resource( |
|
45 |
Resource, |
|
46 |
title='Test 1', |
|
47 |
slug='test1', |
|
48 |
description='Connecteur de test', |
|
49 |
input_sftp='sftp://john:doe@{server.host}:{server.port}/DILA/'.format(server=sftpserver), |
|
50 |
output_sftp='sftp://john:doe@{server.host}:{server.port}/DILA/'.format(server=sftpserver) |
|
51 |
) |
|
52 | ||
53 | ||
54 |
def test_resource(spfr): |
|
55 |
from passerelle.utils.wcs import get_wcs_choices |
|
56 | ||
57 |
assert get_wcs_choices() == [] |
|
58 | ||
59 | ||
60 |
def test_sftp_access(spfr, sftpserver): |
|
61 |
with sftpserver.serve_content(DUMMY_CONTENT): |
|
62 |
with spfr.input_sftp as input_sftp: |
|
63 |
assert input_sftp.listdir() == ['a.zip'] |
|
64 |
with spfr.output_sftp as output_sftp: |
|
65 |
assert output_sftp.listdir() == ['a.zip'] |
|
0 |
- |