0001-general-remove-sp_fr-connector-68918.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/admin.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.contrib import admin |
|
18 |
from django.utils.html import format_html |
|
19 | ||
20 |
from .models import Request |
|
21 | ||
22 | ||
23 |
class RequestAdmin(admin.ModelAdmin): |
|
24 |
date_hierarchy = 'created' |
|
25 |
search_fields = ['url', 'filename'] |
|
26 |
list_display = ['id', 'created', 'modified', 'state', 'filename', 'form_url'] |
|
27 | ||
28 |
def form_url(self, obj): |
|
29 |
return format_html('<a href="{0}">{0}</a>', obj.url) |
|
30 | ||
31 |
form_url.allow_tags = True |
|
32 | ||
33 | ||
34 |
admin.site.register(Request, RequestAdmin) |
passerelle/apps/sp_fr/depotDossierPACS.XSD | ||
---|---|---|
1 |
<?xml version="1.0" encoding="UTF-8"?> |
|
2 |
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|
3 |
<xs:element name="PACS" type="PacsType"/> |
|
4 |
<xs:complexType name="PacsType"> |
|
5 |
<xs:sequence> |
|
6 |
<xs:element name="partenaire1" type="PartenaireType" /> |
|
7 |
<xs:element name="partenaire2" type="PartenaireType" /> |
|
8 |
<xs:element name="convention" type="ConventionType" maxOccurs="1" minOccurs="1" /> |
|
9 |
<xs:element name="residenceCommune" type="AdresseType" /> |
|
10 |
<xs:element name="attestationHonneur" type="AttestationHonneurType" /> |
|
11 |
</xs:sequence> |
|
12 |
</xs:complexType> |
|
13 |
<xs:complexType name = "AttestationHonneurType"> |
|
14 |
<xs:sequence> |
|
15 |
<xs:element name="nonParente" type="xs:boolean"/> |
|
16 |
<xs:element name="residenceCommune" type="xs:boolean"/> |
|
17 |
</xs:sequence> |
|
18 |
</xs:complexType> |
|
19 |
<xs:complexType name="PartenaireType"> |
|
20 |
<xs:sequence> |
|
21 |
<xs:element name="civilite" type="CiviliteType"></xs:element> |
|
22 |
<xs:element name="nomNaissance" type="xs:string" /> |
|
23 |
<xs:element name="prenoms" type="xs:string" /> |
|
24 |
<xs:element name="codeNationalite" type="xs:string" maxOccurs="unbounded"/> |
|
25 |
<xs:element name="jourNaissance" type="xs:integer" maxOccurs="1" minOccurs="0" /> |
|
26 |
<xs:element name="moisNaissance" type="xs:integer" maxOccurs="1" minOccurs="0" /> |
|
27 |
<xs:element name="anneeNaissance" type="xs:integer" /> |
|
28 |
<xs:element name="LieuNaissance" type="LieuNaissanceType" /> |
|
29 |
<xs:element name="ofpra" type="xs:boolean" /> |
|
30 |
<xs:element name="mesureJuridique" type="xs:boolean" /> |
|
31 |
<xs:element name="adressePostale" type="AdresseType" /> |
|
32 |
<xs:element name="adresseElectronique" type="xs:string" /> |
|
33 |
<xs:element name="telephone" type="xs:string" minOccurs="0"/> |
|
34 |
<xs:element name="filiationParent1" type="FiliationType" minOccurs="0"/> |
|
35 |
<xs:element name="filiationParent2" type="FiliationType" minOccurs="0" /> |
|
36 |
<xs:element name="titreIdentiteVerifie" type="xs:boolean"/> |
|
37 |
</xs:sequence> |
|
38 |
</xs:complexType> |
|
39 |
<xs:complexType name="ConventionType"> |
|
40 |
<xs:choice> |
|
41 |
<xs:element name="conventionType" type="ConventionTypeType" /> |
|
42 |
<xs:element name="conventionSpecifique" type="xs:boolean" /> |
|
43 |
</xs:choice> |
|
44 |
</xs:complexType> |
|
45 |
<xs:complexType name="ConventionTypeType"> |
|
46 |
<xs:sequence> |
|
47 |
<xs:element name="aideMaterielMontant" type="xs:double" maxOccurs="1" minOccurs="0"/> |
|
48 |
<xs:element name="regimePacs" type="regimePacsType" /> |
|
49 |
<xs:element name="aideMateriel" type="AideMaterielType" /> |
|
50 |
</xs:sequence> |
|
51 |
</xs:complexType> |
|
52 |
<xs:complexType name="AdresseType"> |
|
53 |
<xs:sequence> |
|
54 |
<xs:element name="NumeroLibelleVoie" type="xs:string" minOccurs="0" /> |
|
55 |
<xs:element name="Complement1" type="xs:string" minOccurs="0" /> |
|
56 |
<xs:element name="Complement2" type="xs:string" minOccurs="0" /> |
|
57 |
<xs:element name="LieuDitBpCommuneDeleguee" type="xs:string" minOccurs="0" /> |
|
58 |
<xs:element name="CodePostal" type="codePostalType" /> |
|
59 |
<xs:element name="Localite" type="localiteType" /> |
|
60 |
<xs:element name="Pays" type="xs:string" /> |
|
61 |
</xs:sequence> |
|
62 |
</xs:complexType> |
|
63 |
<xs:complexType name="LieuNaissanceType"> |
|
64 |
<xs:sequence> |
|
65 |
<xs:element name="localite" type="localiteType"/> |
|
66 |
<xs:element name="codePostal" type="xs:string"/> |
|
67 |
<xs:element name="codeInsee" type="xs:string" minOccurs="0"/> |
|
68 |
<xs:element name="departement" type="xs:string" maxOccurs="1" minOccurs="0"/> |
|
69 |
<xs:element name="codePays" type="xs:string"/> |
|
70 |
</xs:sequence> |
|
71 |
</xs:complexType> |
|
72 |
<xs:simpleType name="localiteType"> |
|
73 |
<xs:restriction base="xs:string"> |
|
74 |
<xs:minLength value="1" /> |
|
75 |
</xs:restriction> |
|
76 |
</xs:simpleType> |
|
77 |
<xs:simpleType name="codePostalType"> |
|
78 |
<xs:restriction base="xs:string"> |
|
79 |
<xs:length value="5" /> |
|
80 |
</xs:restriction> |
|
81 |
</xs:simpleType> |
|
82 |
<xs:simpleType name="regimePacsType"> |
|
83 |
<xs:restriction base="xs:string"> |
|
84 |
<xs:enumeration value="indivision"/> |
|
85 |
<xs:enumeration value="legal"/> |
|
86 |
</xs:restriction> |
|
87 |
</xs:simpleType> |
|
88 |
<xs:complexType name="FiliationType"> |
|
89 |
<xs:sequence> |
|
90 |
<xs:choice> |
|
91 |
<xs:element name="filiationInconnu" type="xs:boolean"></xs:element> |
|
92 |
<xs:element name="filiationConnu" type="FiliationConnuType"> |
|
93 |
</xs:element> |
|
94 |
</xs:choice> |
|
95 |
</xs:sequence> |
|
96 |
</xs:complexType> |
|
97 |
<xs:simpleType name="CiviliteType"> |
|
98 |
<xs:restriction base="xs:string"> |
|
99 |
<xs:enumeration value="M"></xs:enumeration> |
|
100 |
<xs:enumeration value="MME"></xs:enumeration> |
|
101 |
</xs:restriction> |
|
102 |
</xs:simpleType> |
|
103 |
<xs:simpleType name="TypeAideMaterielType"> |
|
104 |
<xs:restriction base="xs:string"> |
|
105 |
<xs:enumeration value="aideFixe"/> |
|
106 |
<xs:enumeration value="aideProportionnel"/> |
|
107 |
</xs:restriction> |
|
108 |
</xs:simpleType> |
|
109 |
<xs:complexType name="AideMaterielType"> |
|
110 |
<xs:sequence> |
|
111 |
<xs:element name="typeAideMateriel" type="TypeAideMaterielType"></xs:element> |
|
112 |
</xs:sequence> |
|
113 |
</xs:complexType> |
|
114 |
<xs:complexType name="FiliationConnuType"> |
|
115 |
<xs:sequence> |
|
116 |
<xs:element name="sexe" type="SexeType"/> |
|
117 |
<xs:element name="nomNaissance" type="xs:string" maxOccurs="1" minOccurs="0" /> |
|
118 |
<xs:element name="prenoms" type="xs:string" maxOccurs="1" minOccurs="0" /> |
|
119 |
<xs:element name="dateNaissance" type="xs:string" maxOccurs="1" minOccurs="0" /> |
|
120 |
<xs:element name="lieuNaissance" type="LieuNaissanceType" maxOccurs="1" minOccurs="0" /> |
|
121 |
</xs:sequence> |
|
122 |
</xs:complexType> |
|
123 |
<xs:simpleType name="SexeType"> |
|
124 |
<xs:restriction base="xs:string"> |
|
125 |
<xs:enumeration value="M"/> |
|
126 |
<xs:enumeration value="F"/> |
|
127 |
</xs:restriction> |
|
128 |
</xs:simpleType> |
|
129 |
</xs:schema> |
passerelle/apps/sp_fr/fields.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 |
from django.core.exceptions import ValidationError |
|
19 |
from django.template import TemplateSyntaxError, engines |
|
20 | ||
21 | ||
22 |
def validate_django_template(value): |
|
23 |
try: |
|
24 |
engines['django'].from_string(value) |
|
25 |
except TemplateSyntaxError as e: |
|
26 |
raise ValidationError('invalid template %s' % e) |
|
27 | ||
28 | ||
29 |
class VariableAndExpressionWidget(forms.MultiWidget): |
|
30 |
template_name = 'passerelle/widgets/variable_and_expression_widget.html' |
|
31 | ||
32 |
def __init__(self, **kwargs): |
|
33 |
widgets = [ |
|
34 |
forms.Select, |
|
35 |
forms.TextInput, |
|
36 |
] |
|
37 |
super().__init__(widgets=widgets, **kwargs) |
|
38 | ||
39 |
def decompress(self, value): |
|
40 |
if not value: |
|
41 |
return [None, None] |
|
42 |
return value['variable'], value['expression'] |
|
43 | ||
44 |
# XXX: bug in Django https://code.djangoproject.com/ticket/29205 |
|
45 |
# required_attribute is initialized from the parent.field required |
|
46 |
# attribute and not from each sub-field attribute |
|
47 |
def use_required_attribute(self, initial): |
|
48 |
return False |
|
49 | ||
50 | ||
51 |
class VariableAndExpressionField(forms.MultiValueField): |
|
52 |
widget = VariableAndExpressionWidget |
|
53 | ||
54 |
def __init__( |
|
55 |
self, choices=(), required=True, widget=None, label=None, initial=None, help_text='', *args, **kwargs |
|
56 |
): |
|
57 |
fields = [ |
|
58 |
forms.ChoiceField(choices=choices, required=required), |
|
59 |
forms.CharField(required=False, validators=[validate_django_template]), |
|
60 |
] |
|
61 |
super().__init__( |
|
62 |
fields=fields, |
|
63 |
required=required, |
|
64 |
widget=widget, |
|
65 |
label=label, |
|
66 |
initial=initial, |
|
67 |
help_text=help_text, |
|
68 |
require_all_fields=False, |
|
69 |
*args, |
|
70 |
**kwargs, |
|
71 |
) |
|
72 |
self.choices = choices |
|
73 | ||
74 |
def _get_choices(self): |
|
75 |
return self._choices |
|
76 | ||
77 |
def _set_choices(self, value): |
|
78 |
# Setting choices also sets the choices on the widget. |
|
79 |
# choices can be any iterable, but we call list() on it because |
|
80 |
# it will be consumed more than once. |
|
81 |
if callable(value): |
|
82 |
value = forms.CallableChoiceIterator(value) |
|
83 |
else: |
|
84 |
value = list(value) |
|
85 |
self._choices = value |
|
86 |
self.widget.widgets[0].choices = value |
|
87 | ||
88 |
choices = property(_get_choices, _set_choices) |
|
89 | ||
90 |
def compress(self, data_list): |
|
91 |
try: |
|
92 |
variable, expression = data_list |
|
93 |
except (ValueError, TypeError): |
|
94 |
return None |
|
95 |
else: |
|
96 |
return { |
|
97 |
'variable': variable, |
|
98 |
'expression': expression, |
|
99 |
} |
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 fields, models |
|
20 | ||
21 | ||
22 |
class MappingForm(forms.ModelForm): |
|
23 |
def __init__(self, *args, **kwargs): |
|
24 |
super().__init__(*args, **kwargs) |
|
25 |
if self.instance.procedure and self.instance and self.instance.formdef: |
|
26 |
choices = [('', '--------')] + [(v, v) for v in self.instance.variables] |
|
27 |
for i, field in enumerate(self.schema_fields()): |
|
28 |
label = field.label |
|
29 |
label += ' (%s)' % (field.varname or 'NO VARNAME') |
|
30 |
base_name = str(field.varname or i) |
|
31 |
initial = self.instance.rules.get('fields', {}).get(base_name) |
|
32 |
self.fields['field_%s' % base_name] = fields.VariableAndExpressionField( |
|
33 |
label=label, choices=choices, initial=initial, required=False |
|
34 |
) |
|
35 | ||
36 |
def table_fields(self): |
|
37 |
return [field for field in self if field.name.startswith('field_')] |
|
38 | ||
39 |
def schema_fields(self): |
|
40 |
if self.instance and self.instance.formdef: |
|
41 |
schema = self.instance.formdef.schema |
|
42 |
for i, field in enumerate(schema.fields): |
|
43 |
if field.type in ('page', 'comment', 'title', 'subtitle'): |
|
44 |
continue |
|
45 |
yield field |
|
46 | ||
47 |
def save(self, commit=True): |
|
48 |
fields = {} |
|
49 |
for key in self.cleaned_data: |
|
50 |
if not key.startswith('field_'): |
|
51 |
continue |
|
52 |
if not self.cleaned_data[key]: |
|
53 |
continue |
|
54 |
real_key = key[len('field_') :] |
|
55 |
value = self.cleaned_data[key].copy() |
|
56 |
value['label'] = self.fields[key].label |
|
57 |
fields[real_key] = value |
|
58 |
self.instance.rules['fields'] = fields |
|
59 |
return super().save(commit=commit) |
|
60 | ||
61 |
class Meta: |
|
62 |
model = models.Mapping |
|
63 |
fields = [ |
|
64 |
'procedure', |
|
65 |
'formdef', |
|
66 |
] |
passerelle/apps/sp_fr/migrations/0001_initial.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.20 on 2019-04-19 17:15 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
import django.contrib.postgres.fields.jsonb |
|
6 |
import django.db.models.deletion |
|
7 |
from django.db import migrations, models |
|
8 | ||
9 |
import passerelle.apps.sp_fr.models |
|
10 |
import passerelle.utils.sftp |
|
11 |
import passerelle.utils.wcs |
|
12 | ||
13 | ||
14 |
class Migration(migrations.Migration): |
|
15 | ||
16 |
initial = True |
|
17 | ||
18 |
dependencies = [ |
|
19 |
('base', '0012_job'), |
|
20 |
] |
|
21 | ||
22 |
operations = [ |
|
23 |
migrations.CreateModel( |
|
24 |
name='Mapping', |
|
25 |
fields=[ |
|
26 |
( |
|
27 |
'id', |
|
28 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
29 |
), |
|
30 |
( |
|
31 |
'procedure', |
|
32 |
models.CharField( |
|
33 |
choices=[ |
|
34 |
(b'DOC', 'Request for construction site opening'), |
|
35 |
(b'recensementCitoyen', 'Request for mandatory citizen census'), |
|
36 |
(b'depotDossierPACS', 'Pre-request for citizen solidarity pact'), |
|
37 |
], |
|
38 |
max_length=32, |
|
39 |
unique=True, |
|
40 |
verbose_name='Procedure', |
|
41 |
), |
|
42 |
), |
|
43 |
('formdef', passerelle.utils.wcs.FormDefField(verbose_name='Formdef')), |
|
44 |
( |
|
45 |
'rules', |
|
46 |
django.contrib.postgres.fields.jsonb.JSONField( |
|
47 |
default=passerelle.apps.sp_fr.models.default_rule, verbose_name='Rules' |
|
48 |
), |
|
49 |
), |
|
50 |
], |
|
51 |
options={ |
|
52 |
'verbose_name': 'MDEL mapping', |
|
53 |
'verbose_name_plural': 'MDEL mappings', |
|
54 |
}, |
|
55 |
), |
|
56 |
migrations.CreateModel( |
|
57 |
name='Request', |
|
58 |
fields=[ |
|
59 |
( |
|
60 |
'id', |
|
61 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
62 |
), |
|
63 |
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), |
|
64 |
('modified', models.DateTimeField(auto_now=True, verbose_name='Created')), |
|
65 |
('filename', models.CharField(max_length=128, verbose_name='Identifier')), |
|
66 |
('archive', models.FileField(max_length=256, upload_to=b'', verbose_name='Archive')), |
|
67 |
( |
|
68 |
'state', |
|
69 |
models.CharField( |
|
70 |
choices=[ |
|
71 |
(b'received', 'Received'), |
|
72 |
(b'transfered', 'Transferred'), |
|
73 |
(b'error', 'Error'), |
|
74 |
(b'returned', 'Returned'), |
|
75 |
], |
|
76 |
default=b'received', |
|
77 |
max_length=16, |
|
78 |
verbose_name='State', |
|
79 |
), |
|
80 |
), |
|
81 |
('url', models.URLField(blank=True, verbose_name='URL')), |
|
82 |
], |
|
83 |
options={ |
|
84 |
'verbose_name': 'MDEL request', |
|
85 |
'verbose_name_plural': 'MDEL requests', |
|
86 |
}, |
|
87 |
), |
|
88 |
migrations.CreateModel( |
|
89 |
name='Resource', |
|
90 |
fields=[ |
|
91 |
( |
|
92 |
'id', |
|
93 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
94 |
), |
|
95 |
('title', models.CharField(max_length=50, verbose_name='Title')), |
|
96 |
('description', models.TextField(verbose_name='Description')), |
|
97 |
('slug', models.SlugField(unique=True, verbose_name='Identifier')), |
|
98 |
( |
|
99 |
'input_sftp', |
|
100 |
passerelle.utils.sftp.SFTPField(default=None, null=True, verbose_name='Input SFTP URL'), |
|
101 |
), |
|
102 |
( |
|
103 |
'output_sftp', |
|
104 |
passerelle.utils.sftp.SFTPField(default=None, null=True, verbose_name='Output SFTP URL'), |
|
105 |
), |
|
106 |
( |
|
107 |
'users', |
|
108 |
models.ManyToManyField( |
|
109 |
blank=True, |
|
110 |
related_name='_resource_users_+', |
|
111 |
related_query_name='+', |
|
112 |
to='base.ApiUser', |
|
113 |
), |
|
114 |
), |
|
115 |
], |
|
116 |
options={ |
|
117 |
'verbose_name': 'Service-Public.fr', |
|
118 |
}, |
|
119 |
), |
|
120 |
migrations.AddField( |
|
121 |
model_name='request', |
|
122 |
name='resource', |
|
123 |
field=models.ForeignKey( |
|
124 |
on_delete=django.db.models.deletion.CASCADE, to='sp_fr.Resource', verbose_name='Resource' |
|
125 |
), |
|
126 |
), |
|
127 |
migrations.AddField( |
|
128 |
model_name='mapping', |
|
129 |
name='resource', |
|
130 |
field=models.ForeignKey( |
|
131 |
on_delete=django.db.models.deletion.CASCADE, |
|
132 |
related_name='mappings', |
|
133 |
to='sp_fr.Resource', |
|
134 |
verbose_name='Resource', |
|
135 |
), |
|
136 |
), |
|
137 |
migrations.AlterUniqueTogether( |
|
138 |
name='request', |
|
139 |
unique_together=set([('resource', 'filename')]), |
|
140 |
), |
|
141 |
] |
passerelle/apps/sp_fr/migrations/0002_auto_20200504_1402.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-05-04 12:02 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('sp_fr', '0001_initial'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AlterField( |
|
16 |
model_name='mapping', |
|
17 |
name='procedure', |
|
18 |
field=models.CharField( |
|
19 |
choices=[ |
|
20 |
('DOC', 'Request for construction site opening'), |
|
21 |
('recensementCitoyen', 'Request for mandatory citizen census'), |
|
22 |
('depotDossierPACS', 'Pre-request for citizen solidarity pact'), |
|
23 |
], |
|
24 |
max_length=32, |
|
25 |
unique=True, |
|
26 |
verbose_name='Procedure', |
|
27 |
), |
|
28 |
), |
|
29 |
migrations.AlterField( |
|
30 |
model_name='request', |
|
31 |
name='archive', |
|
32 |
field=models.FileField(max_length=256, upload_to='', verbose_name='Archive'), |
|
33 |
), |
|
34 |
migrations.AlterField( |
|
35 |
model_name='request', |
|
36 |
name='state', |
|
37 |
field=models.CharField( |
|
38 |
choices=[ |
|
39 |
('received', 'Received'), |
|
40 |
('transfered', 'Transferred'), |
|
41 |
('error', 'Error'), |
|
42 |
('returned', 'Returned'), |
|
43 |
], |
|
44 |
default='received', |
|
45 |
max_length=16, |
|
46 |
verbose_name='State', |
|
47 |
), |
|
48 |
), |
|
49 |
] |
passerelle/apps/sp_fr/migrations/0003_text_to_jsonb.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-05-04 12:06 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations |
|
6 | ||
7 |
from passerelle.utils.db import EnsureJsonbType |
|
8 | ||
9 | ||
10 |
class Migration(migrations.Migration): |
|
11 | ||
12 |
dependencies = [ |
|
13 |
('sp_fr', '0002_auto_20200504_1402'), |
|
14 |
] |
|
15 | ||
16 |
operations = [ |
|
17 |
EnsureJsonbType(model_name='Mapping', field_name='rules'), |
|
18 |
] |
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 base64 |
|
19 |
import collections |
|
20 |
import datetime |
|
21 |
import os |
|
22 |
import re |
|
23 |
import stat |
|
24 |
import zipfile |
|
25 | ||
26 |
from django.contrib.postgres.fields import JSONField |
|
27 |
from django.core.files import File |
|
28 |
from django.db import models, transaction |
|
29 |
from django.template import engines |
|
30 |
from django.urls import reverse |
|
31 |
from django.utils.translation import gettext |
|
32 |
from django.utils.translation import gettext_lazy as _ |
|
33 |
from lxml import etree as ET |
|
34 | ||
35 |
from passerelle.base.models import BaseResource |
|
36 |
from passerelle.utils.api import endpoint |
|
37 |
from passerelle.utils.conversion import normalize |
|
38 |
from passerelle.utils.sftp import SFTPField |
|
39 |
from passerelle.utils.wcs import FormDefField, get_wcs_choices |
|
40 |
from passerelle.utils.xml import text_content |
|
41 | ||
42 |
from .xsd import Schema |
|
43 | ||
44 |
MAX_REQUESTS_PER_ITERATION = 200 |
|
45 | ||
46 |
PROCEDURE_DOC = 'DOC' |
|
47 |
PROCEDURE_RCO = 'recensementCitoyen' |
|
48 |
PROCEDURE_DDPACS = 'depotDossierPACS' |
|
49 |
PROCEDURES = [ |
|
50 |
(PROCEDURE_DOC, _('Request for construction site opening')), |
|
51 |
(PROCEDURE_RCO, _('Request for mandatory citizen census')), |
|
52 |
(PROCEDURE_DDPACS, _('Pre-request for citizen solidarity pact')), |
|
53 |
] |
|
54 | ||
55 |
FILE_PATTERN = re.compile(r'^(?P<identifier>.*)-(?P<procedure>[a-zA-Z0-9]+)-(?P<sequence>\d+).zip$') |
|
56 |
ENT_PATTERN = re.compile(r'^.*-ent-\d+(?:-.*)?.xml$') |
|
57 |
NSMAP = {'dgme-metier': 'http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier'} |
|
58 |
ROUTAGE_XPATH = ET.XPath( |
|
59 |
('dgme-metier:Routage/dgme-metier:Donnee/dgme-metier:Valeur/text()'), namespaces=NSMAP |
|
60 |
) |
|
61 | ||
62 |
EMAIL_XPATH = ET.XPath(('dgme-metier:Teledemarche/dgme-metier:Email/text()'), namespaces=NSMAP) |
|
63 | ||
64 |
DOCUMENTS_XPATH = ET.XPath('dgme-metier:Document', namespaces=NSMAP) |
|
65 |
PIECE_JOINTE_XPATH = ET.XPath('dgme-metier:PieceJointe', namespaces=NSMAP) |
|
66 |
CODE_XPATH = ET.XPath('dgme-metier:Code', namespaces=NSMAP) |
|
67 |
FICHIER_XPATH = ET.XPath('dgme-metier:Fichier', namespaces=NSMAP) |
|
68 |
FICHIER_DONNEES_XPATH = ET.XPath('.//dgme-metier:FichierDonnees', namespaces=NSMAP) |
|
69 | ||
70 |
ET.register_namespace( |
|
71 |
'dgme-metier', 'http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier' |
|
72 |
) |
|
73 | ||
74 | ||
75 |
def simplify(s): |
|
76 |
'''Simplify XML node tag names because XSD from DGME are garbage''' |
|
77 |
if not s: |
|
78 |
return '' |
|
79 |
if not isinstance(s, str): |
|
80 |
s = str(s, 'utf-8', 'ignore') |
|
81 |
s = normalize(s) |
|
82 |
s = re.sub(r'[^\w\s\'-_]', '', s) |
|
83 |
s = s.replace('-', '_') |
|
84 |
s = re.sub(r'[\s\']+', '', s) |
|
85 |
return s.strip().lower() |
|
86 | ||
87 | ||
88 |
class Resource(BaseResource): |
|
89 |
category = _('Business Process Connectors') |
|
90 | ||
91 |
input_sftp = SFTPField(verbose_name=_('Input SFTP URL'), null=True) |
|
92 | ||
93 |
output_sftp = SFTPField(verbose_name=_('Output SFTP URL'), null=True) |
|
94 | ||
95 |
def check_status(self): |
|
96 |
with self.input_sftp.client() as sftp: |
|
97 |
sftp.listdir() |
|
98 |
with self.output_sftp.client() as sftp: |
|
99 |
sftp.listdir() |
|
100 |
get_wcs_choices(session=self.requests) |
|
101 | ||
102 |
@endpoint(name='ping', show=False, description=_('Check SFTP availability')) |
|
103 |
def ping(self, request): |
|
104 |
# deprecated endpoint |
|
105 |
self.check_status() |
|
106 |
return {'err': 0} |
|
107 | ||
108 |
def hourly(self): |
|
109 |
self.run_loop() |
|
110 | ||
111 |
def run_loop(self, count=0): |
|
112 |
if count == 0: |
|
113 |
count = MAX_REQUESTS_PER_ITERATION |
|
114 |
with transaction.atomic(): |
|
115 |
# lock resource |
|
116 |
r = Resource.objects.select_for_update(skip_locked=True).filter(pk=self.pk) |
|
117 |
if not r: |
|
118 |
# already locked |
|
119 |
self.logger.info('did nothing') |
|
120 |
return |
|
121 |
with self.input_sftp.client() as sftp: |
|
122 |
try: |
|
123 |
sftp.lstat('DONE') |
|
124 |
except IOError: |
|
125 |
sftp.mkdir('DONE') |
|
126 | ||
127 |
try: |
|
128 |
sftp.lstat('FAILED') |
|
129 |
except IOError: |
|
130 |
sftp.mkdir('FAILED') |
|
131 | ||
132 |
def helper(): |
|
133 |
for file_stat in sftp.listdir_attr(): |
|
134 |
if stat.S_ISDIR(file_stat.st_mode): |
|
135 |
continue |
|
136 |
yield file_stat.filename |
|
137 | ||
138 |
for filename in helper(): |
|
139 |
m = FILE_PATTERN.match(filename) |
|
140 |
if not m: |
|
141 |
self.logger.info( |
|
142 |
'file "%s" did not match pattern %s, moving to FAILED/', filename, FILE_PATTERN |
|
143 |
) |
|
144 |
sftp.rename(filename, 'FAILED/' + filename) |
|
145 |
continue |
|
146 |
procedure = m.group('procedure') |
|
147 |
try: |
|
148 |
mapping = self.mappings.get(procedure=procedure) |
|
149 |
except Mapping.DoesNotExist: |
|
150 |
self.logger.info( |
|
151 |
'no mapping for procedure "%s" for file "%s", moving to FAILED/', |
|
152 |
procedure, |
|
153 |
filename, |
|
154 |
) |
|
155 |
continue |
|
156 | ||
157 |
handler = self.FileHandler( |
|
158 |
resource=self, |
|
159 |
sftp=sftp, |
|
160 |
filename=filename, |
|
161 |
identifier=m.group('identifier'), |
|
162 |
procedure=procedure, |
|
163 |
sequence=m.group('sequence'), |
|
164 |
mapping=mapping, |
|
165 |
) |
|
166 |
if not handler.request: |
|
167 |
count -= 1 |
|
168 |
try: |
|
169 |
move, error = handler() |
|
170 |
except Exception: |
|
171 |
count -= 1 |
|
172 |
self.logger.exception('handling of file "%s" failed', filename) |
|
173 |
sftp.rename(filename, 'FAILED/' + filename) |
|
174 |
else: |
|
175 |
if move and error: |
|
176 |
count -= 1 |
|
177 |
self.logger.error('handling of file "%s" failed: %s', filename, error) |
|
178 |
sftp.rename(filename, 'FAILED/' + filename) |
|
179 |
else: |
|
180 |
if error: |
|
181 |
count -= 1 |
|
182 |
self.logger.warning('handling of file "%s" failed: %s', filename, error) |
|
183 |
elif move: |
|
184 |
count -= 1 |
|
185 |
sftp.rename(filename, 'DONE/' + filename) |
|
186 |
if not count: |
|
187 |
break |
|
188 | ||
189 |
class FileHandler: |
|
190 |
def __init__(self, resource, sftp, filename, identifier, procedure, sequence, mapping): |
|
191 |
self.resource = resource |
|
192 |
self.sftp = sftp |
|
193 |
self.filename = filename |
|
194 |
self.identifier = identifier |
|
195 |
self.procedure = procedure |
|
196 |
self.sequence = sequence |
|
197 |
self.mapping = mapping |
|
198 |
self.variables = list(self.mapping.variables) |
|
199 |
self.request = Request.objects.filter(resource=resource, filename=filename).first() |
|
200 | ||
201 |
def __call__(self): |
|
202 |
if not self.request: |
|
203 |
with self.sftp.open(self.filename) as fd: |
|
204 |
with transaction.atomic(): |
|
205 |
self.request = Request.objects.create(resource=self.resource, filename=self.filename) |
|
206 |
self.request.state = Request.STATE_RECEIVED |
|
207 |
self.request.archive.save(self.filename, File(fd)) |
|
208 |
if self.request.state == Request.STATE_RECEIVED: |
|
209 |
with self.request.archive as fd: |
|
210 |
# error during processing are fatal, we want to log them |
|
211 |
data, error = self.process(fd) |
|
212 |
if not data: |
|
213 |
return False, error |
|
214 |
try: |
|
215 |
backoffice_url = self.transfer(data) |
|
216 |
except Exception as e: |
|
217 |
return False, 'error during transfer to w.c.s %r' % e |
|
218 |
self.request.url = backoffice_url |
|
219 |
self.request.state = Request.STATE_TRANSFERED |
|
220 |
self.request.save() |
|
221 | ||
222 |
if self.request.state == Request.STATE_TRANSFERED: |
|
223 |
try: |
|
224 |
self.response() |
|
225 |
except Exception as e: |
|
226 |
return False, 'error during response to service-public.fr %r' % e |
|
227 |
self.request.state = Request.STATE_RETURNED |
|
228 |
self.request.save() |
|
229 |
self.resource.logger.info('%s responded, closed', self.request.filename) |
|
230 |
return True, None |
|
231 | ||
232 |
def process(self, fd): |
|
233 |
try: |
|
234 |
with zipfile.ZipFile(fd) as archive: |
|
235 |
# sort files |
|
236 |
doc_files = [] |
|
237 |
ent_files = [] |
|
238 |
attachments = {} |
|
239 |
for name in archive.namelist(): |
|
240 |
if ENT_PATTERN.match(name): |
|
241 |
ent_files.append(name) |
|
242 | ||
243 |
if len(ent_files) != 1: |
|
244 |
return False, 'too many/few ent files found: %s' % ent_files |
|
245 | ||
246 |
ent_file = ent_files[0] |
|
247 | ||
248 |
with archive.open(ent_file) as fd: |
|
249 |
document = ET.parse(fd) |
|
250 | ||
251 |
for pj_node in PIECE_JOINTE_XPATH(document): |
|
252 |
code = CODE_XPATH(pj_node)[0].text |
|
253 |
code = 'pj_' + code.lower().replace('-', '_') |
|
254 |
fichier = FICHIER_XPATH(pj_node)[0].text |
|
255 |
attachments.setdefault(code, []).append(fichier) |
|
256 |
for doc_node in DOCUMENTS_XPATH(document): |
|
257 |
code = CODE_XPATH(doc_node)[0].text |
|
258 |
code = 'doc_' + code.lower().replace('-', '_') |
|
259 |
fichier = FICHIER_DONNEES_XPATH(doc_node)[0].text |
|
260 |
attachments.setdefault(code, []).append(fichier) |
|
261 | ||
262 |
doc_files = [ |
|
263 |
value for l in attachments.values() for value in l if value.lower().endswith('.xml') |
|
264 |
] |
|
265 |
if len(doc_files) != 1: |
|
266 |
return False, 'too many/few doc files found: %s' % doc_files |
|
267 | ||
268 |
for key, attachment in attachments.items(): |
|
269 |
if len(attachment) > 1: |
|
270 |
return False, 'too many attachments of kind %s: %r' % (key, attachment) |
|
271 |
name = attachment[0] |
|
272 |
with archive.open(name) as zip_fd: |
|
273 |
content = zip_fd.read() |
|
274 |
attachments[key] = { |
|
275 |
'filename': name, |
|
276 |
'content': base64.b64encode(content).decode('ascii'), |
|
277 |
'content_type': 'application/octet-stream', |
|
278 |
} |
|
279 | ||
280 |
if self.procedure == PROCEDURE_RCO and not attachments: |
|
281 |
return False, 'no attachments but RCO requires them' |
|
282 | ||
283 |
doc_file = doc_files[0] |
|
284 | ||
285 |
insee_codes = ROUTAGE_XPATH(document) |
|
286 |
if len(insee_codes) != 1: |
|
287 |
return False, 'too many/few insee codes found: %s' % insee_codes |
|
288 |
insee_code = insee_codes[0] |
|
289 | ||
290 |
email = EMAIL_XPATH(document) |
|
291 |
email = email[0] if email else '' |
|
292 | ||
293 |
data = { |
|
294 |
'insee_code': insee_code, |
|
295 |
'email': email, |
|
296 |
} |
|
297 |
data.update(attachments) |
|
298 | ||
299 |
with archive.open(doc_file) as fd: |
|
300 |
document = ET.parse(fd) |
|
301 |
data.update(self.extract_data(document)) |
|
302 |
if hasattr(self, 'update_data_%s' % self.procedure): |
|
303 |
getattr(self, 'update_data_%s' % self.procedure)(data) |
|
304 |
except zipfile.BadZipfile: |
|
305 |
return False, 'could not load zipfile' |
|
306 |
return data, None |
|
307 | ||
308 |
def transfer(self, data): |
|
309 |
formdef = self.mapping.formdef |
|
310 |
formdef.session = self.resource.requests |
|
311 | ||
312 |
with formdef.submit() as submitter: |
|
313 |
submitter.submission_channel = 'web' |
|
314 |
submitter.submission_context = { |
|
315 |
'mdel_procedure': self.procedure, |
|
316 |
'mdel_identifier': self.identifier, |
|
317 |
'mdel_sequence': self.sequence, |
|
318 |
} |
|
319 |
fields = self.mapping.rules.get('fields', {}) |
|
320 |
for name in fields: |
|
321 |
field = fields[name] |
|
322 |
variable = field['variable'] |
|
323 |
expression = field['expression'] |
|
324 |
value = data.get(variable) |
|
325 |
if expression.strip(): |
|
326 |
template = engines['django'].from_string(expression) |
|
327 |
context = data.copy() |
|
328 |
context['value'] = value |
|
329 |
value = template.render(context) |
|
330 |
if not value: |
|
331 |
continue |
|
332 |
submitter.set(name, value) |
|
333 |
return submitter.result.backoffice_url |
|
334 | ||
335 |
def response(self): |
|
336 |
with self.resource.output_sftp.client() as client: |
|
337 |
with client.open(self.request.response_zip_filename, mode='w') as fd: |
|
338 |
self.request.build_response_zip( |
|
339 |
fd, etat='100', commentaire='Demande transmise à la collectivité' |
|
340 |
) |
|
341 |
with self.resource.input_sftp.client() as client: |
|
342 |
with client.open('DONE/' + self.request.response_zip_filename, mode='w') as fd: |
|
343 |
self.request.build_response_zip( |
|
344 |
fd, etat='100', commentaire='Demande transmise à la collectivité' |
|
345 |
) |
|
346 | ||
347 |
def get_data(self, data, name): |
|
348 |
# prevent error in manual mapping |
|
349 |
assert name in self.variables, 'variable "%s" is unknown' % name |
|
350 |
return data.get(name, '') |
|
351 | ||
352 |
def update_data_DOC(self, data): |
|
353 |
def get(name): |
|
354 |
return self.get_data(data, name) |
|
355 | ||
356 |
numero_permis_construire = get('doc_declarant_designation_permis_numero_permis_construire') |
|
357 |
numero_permis_amenager = get('doc_declarant_designation_permis_numero_permis_amenager') |
|
358 |
data['type_permis'] = ( |
|
359 |
'Un permis de construire' if numero_permis_construire else 'Un permis d\'aménager' |
|
360 |
) |
|
361 |
data['numero_permis'] = numero_permis_construire or numero_permis_amenager |
|
362 |
particulier = get('doc_declarant_identite_type_personne').strip().lower() == 'true' |
|
363 |
data['type_declarant'] = 'Un particulier' if particulier else 'Une personne morale' |
|
364 |
if particulier: |
|
365 |
data['nom'] = get('doc_declarant_identite_personne_physique_nom') |
|
366 |
data['prenoms'] = get('doc_declarant_identite_personne_physique_prenom') |
|
367 |
else: |
|
368 |
data['nom'] = get('doc_declarant_identite_personne_morale_representant_personne_morale_nom') |
|
369 |
data['prenoms'] = get( |
|
370 |
'doc_declarant_identite_personne_morale_representant_personne_morale_prenom' |
|
371 |
) |
|
372 |
mapping = { |
|
373 |
'1000': 'Monsieur', |
|
374 |
'1001': 'Madame', |
|
375 |
'1002': 'Madame et Monsieur', |
|
376 |
} |
|
377 |
if particulier: |
|
378 |
data['civilite_particulier'] = mapping.get( |
|
379 |
get('doc_declarant_identite_personne_physique_civilite'), '' |
|
380 |
) |
|
381 |
else: |
|
382 |
data['civilite_pm'] = mapping.get( |
|
383 |
get('doc_declarant_identite_personne_morale_representant_personne_morale_civilite'), '' |
|
384 |
) |
|
385 |
data['portee'] = ( |
|
386 |
'Pour la totalité des travaux' |
|
387 |
if get('doc_ouverture_chantier_totalite_travaux').lower().strip() == 'true' |
|
388 |
else 'Pour une tranche des travaux' |
|
389 |
) |
|
390 | ||
391 |
def update_data_recensementCitoyen(self, data): |
|
392 |
def get(name): |
|
393 |
return self.get_data(data, name) |
|
394 | ||
395 |
motif = get('recensementcitoyen_formalite_formalitemotifcode_1') or get( |
|
396 |
'recensementcitoyen_formalite_formalitemotifcode_2' |
|
397 |
) |
|
398 |
data['motif'] = {'RECENSEMENT': '1', 'EXEMPTION': '2'}[motif] |
|
399 |
if data['motif'] == '2': |
|
400 |
data['motif_exempte'] = ( |
|
401 |
"Titulaire d'une carte d'invalidité de 80% minimum" |
|
402 |
if get('recensementcitoyen_formalite_formalitemotifcode_2') == 'INFIRME' |
|
403 |
else "Autre situation" |
|
404 |
) |
|
405 |
data['justificatif_exemption'] = get('pj_je') |
|
406 |
data['double_nationalite'] = 'Oui' if get('recensementcitoyen_personne_nationalite') else 'Non' |
|
407 |
data['residence_differente'] = ( |
|
408 |
'Oui' if get('recensementcitoyen_personne_adresseresidence_localite') else 'Non' |
|
409 |
) |
|
410 |
data['civilite'] = 'Monsieur' if get('recensementcitoyen_personne_civilite') == 'M' else 'Madame' |
|
411 | ||
412 |
def get_lieu_naissance(variable, code): |
|
413 |
for idx in ['', '_1', '_2']: |
|
414 |
v = variable + idx |
|
415 |
if get(v + '_code') == code: |
|
416 |
return get(v + '_nom') |
|
417 | ||
418 |
data['cp_naissance'] = get_lieu_naissance('recensementcitoyen_personne_lieunaissance', 'AUTRE') |
|
419 |
data['commune_naissance'] = get_lieu_naissance( |
|
420 |
'recensementcitoyen_personne_lieunaissance', 'COMMUNE' |
|
421 |
) |
|
422 |
data['justificatif_identite'] = get('pj_ji') |
|
423 |
situation_matrimoniale = get('recensementcitoyen_personne_situationfamille_situationmatrimoniale') |
|
424 |
data['situation_familiale'] = { |
|
425 |
'Célibataire': 'Célibataire', |
|
426 |
'Marié': 'Marié(e)', |
|
427 |
}.get(situation_matrimoniale, 'Autres') |
|
428 |
if data['situation_familiale'] == 'Autres': |
|
429 |
data['situation_familiale_precision'] = situation_matrimoniale |
|
430 |
pupille = get('recensementcitoyen_personne_situationfamille_pupille') |
|
431 |
data['pupille'] = 'Oui' if pupille else 'Non' |
|
432 |
data['pupille_categorie'] = { |
|
433 |
'NATION': "Pupille de la nation", |
|
434 |
'ETAT': "Pupille de l'État", |
|
435 |
}.get(pupille) |
|
436 |
for idx in ['', '_1', '_2']: |
|
437 |
code = get('recensementcitoyen_personne_methodecontact%s_canalcode' % idx) |
|
438 |
uri = get('recensementcitoyen_personne_methodecontact%s_uri' % idx) |
|
439 |
if code == 'EMAIL': |
|
440 |
data['courriel'] = uri |
|
441 |
if code == 'TEL': |
|
442 |
data['telephone_fixe'] = uri |
|
443 |
data['justificatif_famille'] = get('pj_jf') |
|
444 |
data['filiation_inconnue_p1'] = not get('recensementcitoyen_filiationpere_nomfamille') |
|
445 |
data['filiation_inconnue_p2'] = not get('recensementcitoyen_filiationmere_nomfamille') |
|
446 |
data['cp_naissance_p1'] = get_lieu_naissance( |
|
447 |
'recensementcitoyen_filiationpere_lieunaissance', 'AUTRE' |
|
448 |
) |
|
449 |
data['cp_naissance_p2'] = get_lieu_naissance( |
|
450 |
'recensementcitoyen_filiationmere_lieunaissance', 'AUTRE' |
|
451 |
) |
|
452 |
data['commune_naissance_p1'] = get_lieu_naissance( |
|
453 |
'recensementcitoyen_filiationpere_lieunaissance', 'COMMUNE' |
|
454 |
) |
|
455 |
data['commune_naissance_p2'] = get_lieu_naissance( |
|
456 |
'recensementcitoyen_filiationmere_lieunaissance', 'COMMUNE' |
|
457 |
) |
|
458 |
for key in data: |
|
459 |
if key.endswith('_datenaissance') and data[key]: |
|
460 |
data[key] = datetime.datetime.strptime(data[key], '%d/%m/%Y').date().strftime('%Y-%m-%d') |
|
461 | ||
462 |
def update_data_depotDossierPACS(self, data): |
|
463 |
def get(name): |
|
464 |
return self.get_data(data, name) |
|
465 | ||
466 |
civilite_p1 = get('pacs_partenaire1_civilite') |
|
467 |
data['civilite_p1'] = 'Monsieur' if civilite_p1 == 'M' else 'Madame' |
|
468 |
data['acte_naissance_p1'] = get('pj_an') |
|
469 |
data['identite_verifiee_p1'] = ( |
|
470 |
'Oui' if get('pacs_partenaire1_titreidentiteverifie') == 'true' else 'Non' |
|
471 |
) |
|
472 | ||
473 |
civilite_p2 = get('pacs_partenaire2_civilite') |
|
474 |
data['civilite_p2'] = 'Monsieur' if civilite_p2 == 'M' else 'Madame' |
|
475 |
data['acte_naissance_p2'] = get('pj_anp') |
|
476 |
data['identite_verifiee_p2'] = ( |
|
477 |
'Oui' if get('pacs_partenaire2_titreidentiteverifie') == 'true' else 'Non' |
|
478 |
) |
|
479 | ||
480 |
data['type_convention'] = '2' if get('pacs_convention_conventionspecifique') == 'true' else '1' |
|
481 |
data['aide_materielle'] = ( |
|
482 |
'1' |
|
483 |
if get('pacs_convention_conventiontype_aidemateriel_typeaidemateriel') == 'aideProportionnel' |
|
484 |
else '2' |
|
485 |
) |
|
486 |
data['regime'] = '1' if get('pacs_convention_conventiontype_regimepacs') == 'legal' else '2' |
|
487 |
data['convention_specifique'] = get('pj_cp') |
|
488 | ||
489 |
def extract_data(self, document): |
|
490 |
'''Convert XML into a dictionnary of values''' |
|
491 |
root = document.getroot() |
|
492 | ||
493 |
def tag_name(node): |
|
494 |
return simplify(ET.QName(node.tag).localname) |
|
495 | ||
496 |
def helper(path, node): |
|
497 |
if len(node): |
|
498 |
tags = collections.Counter(tag_name(child) for child in node) |
|
499 |
counter = collections.Counter() |
|
500 |
for child in node: |
|
501 |
name = tag_name(child) |
|
502 |
if tags[name] > 1: |
|
503 |
counter[name] += 1 |
|
504 |
name += '_%s' % counter[name] |
|
505 |
for p, value in helper(path + [name], child): |
|
506 |
yield p, value |
|
507 |
else: |
|
508 |
yield path, text_content(node) |
|
509 |
# case of multiple nodes |
|
510 |
new_path = path[:-1] + [path[-1] + '_1'] |
|
511 |
yield new_path, text_content(node) |
|
512 | ||
513 |
return {'_'.join(path): value for path, value in helper([tag_name(root)], root)} |
|
514 | ||
515 |
def export_json(self): |
|
516 |
d = super().export_json() |
|
517 |
d['mappings'] = [mapping.export_json() for mapping in self.mappings.all()] |
|
518 |
return d |
|
519 | ||
520 |
@classmethod |
|
521 |
def import_json_real(cls, overwrite, instance, d, **kwargs): |
|
522 |
mappings_json = d.pop('mappings', []) |
|
523 |
instance = super().import_json_real(overwrite, instance, d, **kwargs) |
|
524 |
if instance and overwrite: |
|
525 |
instance.mappings.all().delete() |
|
526 |
for mapping_json in mappings_json: |
|
527 |
Mapping.import_json(mapping_json, instance) |
|
528 |
return instance |
|
529 | ||
530 |
class Meta: |
|
531 |
verbose_name = _('Service-Public.fr') |
|
532 | ||
533 | ||
534 |
def default_rule(): |
|
535 |
return {} |
|
536 | ||
537 | ||
538 |
class Mapping(models.Model): |
|
539 |
resource = models.ForeignKey( |
|
540 |
Resource, verbose_name=_('Resource'), related_name='mappings', on_delete=models.CASCADE |
|
541 |
) |
|
542 | ||
543 |
procedure = models.CharField(verbose_name=_('Procedure'), choices=PROCEDURES, unique=True, max_length=32) |
|
544 | ||
545 |
formdef = FormDefField(verbose_name=_('Formdef')) |
|
546 | ||
547 |
rules = JSONField(verbose_name=_('Rules'), default=default_rule) |
|
548 | ||
549 |
def get_absolute_url(self): |
|
550 |
return reverse('sp-fr-mapping-edit', kwargs=dict(slug=self.resource.slug, pk=self.pk)) |
|
551 | ||
552 |
@property |
|
553 |
def xsd(self): |
|
554 |
path = os.path.join(os.path.dirname(__file__), '%s.XSD' % self.procedure) |
|
555 |
with open(path, 'rb') as fd: |
|
556 |
doc = ET.parse(fd) |
|
557 |
schema = Schema() |
|
558 |
schema.visit(doc.getroot()) |
|
559 |
return schema |
|
560 | ||
561 |
@property |
|
562 |
def variables(self): |
|
563 |
yield 'insee_code' |
|
564 |
yield 'email' |
|
565 |
for path, dummy in self.xsd.paths(): |
|
566 |
names = [simplify(tag.localname) for tag in path] |
|
567 |
yield '_'.join(names) |
|
568 |
if hasattr(self, 'variables_%s' % self.procedure): |
|
569 |
for variable in getattr(self, 'variables_%s' % self.procedure): |
|
570 |
yield variable |
|
571 | ||
572 |
@property |
|
573 |
def variables_DOC(self): |
|
574 |
yield 'type_permis' |
|
575 |
yield 'numero_permis' |
|
576 |
yield 'type_declarant' |
|
577 |
yield 'nom' |
|
578 |
yield 'prenoms' |
|
579 |
yield 'civilite_particulier' |
|
580 |
yield 'civilite_pm' |
|
581 |
yield 'portee' |
|
582 | ||
583 |
@property |
|
584 |
def variables_recensementCitoyen(self): |
|
585 |
yield 'motif' |
|
586 |
yield 'motif_exempte' |
|
587 |
yield 'justificatif_exemption' |
|
588 |
yield 'double_nationalite' |
|
589 |
yield 'residence_differente' |
|
590 |
yield 'civilite' |
|
591 |
yield 'cp_naissance' |
|
592 |
yield 'commune_naissance' |
|
593 |
yield 'pj_je' |
|
594 |
yield 'pj_ji' |
|
595 |
yield 'situation_familiale' |
|
596 |
yield 'situation_familiale_precision' |
|
597 |
yield 'pupille' |
|
598 |
yield 'pupille_categorie' |
|
599 |
yield 'courriel' |
|
600 |
yield 'telephone_fixe' |
|
601 |
yield 'pj_jf' |
|
602 |
yield 'filiation_inconnue_p1' |
|
603 |
yield 'filiation_inconnue_p2' |
|
604 |
yield 'cp_naissance_p1' |
|
605 |
yield 'cp_naissance_p2' |
|
606 |
yield 'commune_naissance_p1' |
|
607 |
yield 'commune_naissance_p2' |
|
608 | ||
609 |
@property |
|
610 |
def variables_depotDossierPACS(self): |
|
611 |
yield 'pj_an' |
|
612 |
yield 'pj_anp' |
|
613 |
yield 'pj_cp' |
|
614 |
yield 'doc_15725_01' |
|
615 |
yield 'doc_flux_pacs' |
|
616 |
yield 'doc_recappdf' |
|
617 |
yield 'civilite_p1' |
|
618 |
yield 'acte_naissance_p1' |
|
619 |
yield 'identite_verifiee_p1' |
|
620 | ||
621 |
yield 'civilite_p2' |
|
622 |
yield 'acte_naissance_p2' |
|
623 |
yield 'identite_verifiee_p2' |
|
624 | ||
625 |
yield 'type_convention' |
|
626 |
yield 'aide_materielle' |
|
627 |
yield 'regime' |
|
628 |
yield 'convention_specifique' |
|
629 | ||
630 |
def __str__(self): |
|
631 |
return gettext('Mapping from "{procedure}" to formdef "{formdef}"').format( |
|
632 |
procedure=self.get_procedure_display(), formdef=self.formdef.title if self.formdef else '-' |
|
633 |
) |
|
634 | ||
635 |
def export_json(self): |
|
636 |
return { |
|
637 |
'procedure': self.procedure, |
|
638 |
'formdef': str(self.formdef), |
|
639 |
'rules': self.rules, |
|
640 |
} |
|
641 | ||
642 |
@classmethod |
|
643 |
def import_json(cls, d, resource): |
|
644 |
mapping = cls.objects.filter(resource=resource, procedure=d['procedure']).first() or cls( |
|
645 |
resource=resource, procedure=d['procedure'] |
|
646 |
) |
|
647 |
mapping.formdef = d['formdef'] |
|
648 |
mapping.rules = d['rules'] |
|
649 |
mapping.save() |
|
650 |
return mapping |
|
651 | ||
652 |
class Meta: |
|
653 |
verbose_name = _('MDEL mapping') |
|
654 |
verbose_name_plural = _('MDEL mappings') |
|
655 | ||
656 | ||
657 |
class Request(models.Model): |
|
658 |
# To prevent mixing errors from analysing archive from s-p.fr and errors |
|
659 |
# from pushing to w.c.s we separate processing with three steps: |
|
660 |
# - receiving, i.e. copying zipfile from SFTP and storing them locally |
|
661 |
# - processing, i.e. openeing the zipfile and extracting content as we need it |
|
662 |
# - transferring, pushing content as a new form in w.c.s. |
|
663 |
STATE_RECEIVED = 'received' |
|
664 |
STATE_TRANSFERED = 'transfered' |
|
665 |
STATE_RETURNED = 'returned' |
|
666 |
STATE_ERROR = 'error' |
|
667 |
STATES = [ |
|
668 |
(STATE_RECEIVED, _('Received')), |
|
669 |
(STATE_TRANSFERED, _('Transferred')), |
|
670 |
(STATE_ERROR, _('Error')), |
|
671 |
(STATE_RETURNED, _('Returned')), |
|
672 |
] |
|
673 | ||
674 |
resource = models.ForeignKey(Resource, verbose_name=_('Resource'), on_delete=models.CASCADE) |
|
675 | ||
676 |
created = models.DateTimeField(verbose_name=_('Created'), auto_now_add=True) |
|
677 | ||
678 |
modified = models.DateTimeField(verbose_name=_('Created'), auto_now=True) |
|
679 | ||
680 |
filename = models.CharField(verbose_name=_('Identifier'), max_length=128) |
|
681 | ||
682 |
archive = models.FileField(verbose_name=_('Archive'), max_length=256) |
|
683 | ||
684 |
state = models.CharField(verbose_name=_('State'), choices=STATES, default=STATE_RECEIVED, max_length=16) |
|
685 | ||
686 |
url = models.URLField(verbose_name=_('URL'), blank=True) |
|
687 | ||
688 |
def delete(self, *args, **kwargs): |
|
689 |
try: |
|
690 |
self.archive.delete() |
|
691 |
except Exception: |
|
692 |
self.resource.logger.error('could not delete %s', self.archive) |
|
693 |
return super().delete(*args, **kwargs) |
|
694 | ||
695 |
@property |
|
696 |
def message_xml(self): |
|
697 |
# FileField can be closed, or open, you never know, and used as a |
|
698 |
# contextmanager, __enter__ does not re-open/re-seek(0) it :/ |
|
699 |
self.archive.open() |
|
700 | ||
701 |
# pylint: disable=not-context-manager |
|
702 |
with self.archive as fd: |
|
703 |
with zipfile.ZipFile(fd) as archive: |
|
704 |
with archive.open('message.xml') as message_xml_fd: |
|
705 |
s = message_xml_fd.read() |
|
706 |
return ET.fromstring(s) |
|
707 | ||
708 |
@property |
|
709 |
def id_enveloppe(self): |
|
710 |
message_xml = self.message_xml |
|
711 |
ns = { |
|
712 |
'pec': 'http://finances.gouv.fr/dgme/pec/message/v1', |
|
713 |
'mdel': 'http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier', |
|
714 |
} |
|
715 |
return message_xml.find('.//{%(pec)s}MessageId' % ns).text.split()[1] |
|
716 | ||
717 |
def build_message_xml_retour(self, etat, commentaire): |
|
718 |
message_xml = self.message_xml |
|
719 | ||
720 |
ns = { |
|
721 |
'pec': 'http://finances.gouv.fr/dgme/pec/message/v1', |
|
722 |
'mdel': 'http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier', |
|
723 |
} |
|
724 | ||
725 |
template = '''<ns2:Message xmlns:ns2="http://finances.gouv.fr/dgme/pec/message/v1" |
|
726 |
xmlns="http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier"> |
|
727 |
<ns2:Header> |
|
728 |
<ns2:Routing> |
|
729 |
<ns2:MessageId/> |
|
730 |
<ns2:RefToMessageId/> |
|
731 |
<ns2:FlowType/> |
|
732 |
<ns2:Sender/> |
|
733 |
<ns2:Recipients> |
|
734 |
<ns2:Recipient/> |
|
735 |
</ns2:Recipients> |
|
736 |
</ns2:Routing> |
|
737 |
<ns2:Security> |
|
738 |
<ns2:Horodatage>false</ns2:Horodatage> |
|
739 |
</ns2:Security> |
|
740 |
</ns2:Header> |
|
741 |
<ns2:Body> |
|
742 |
<ns2:Content><ns2:Retour> |
|
743 |
<ns2:Enveloppe> |
|
744 |
<ns2:NumeroTeledemarche/> |
|
745 |
<ns2:MotDePasse/> |
|
746 |
</ns2:Enveloppe> |
|
747 |
<ns2:Instruction> |
|
748 |
<ns2:Maj> |
|
749 |
<ns2:Etat/> |
|
750 |
<ns2:Commentaire/> |
|
751 |
</ns2:Maj> |
|
752 |
</ns2:Instruction> |
|
753 |
</ns2:Retour> |
|
754 |
</ns2:Content> |
|
755 |
</ns2:Body> |
|
756 |
</ns2:Message>''' |
|
757 | ||
758 |
response = ET.XML(template) |
|
759 | ||
760 |
message_id = message_xml.find('.//{%(pec)s}MessageId' % ns).text |
|
761 |
# maybe could work with str(uuid.uuid4().hex), which would be more unique, we will never know |
|
762 |
response.find('.//{%(pec)s}MessageId' % ns).text = 'RET-1-' + message_id |
|
763 |
response.find('.//{%(pec)s}RefToMessageId' % ns).text = message_id |
|
764 |
response.find('.//{%(pec)s}FlowType' % ns).text = message_xml.find('.//{%(pec)s}FlowType' % ns).text |
|
765 |
response.find('.//{%(pec)s}Sender' % ns).extend(message_xml.find('.//{%(pec)s}Recipient' % ns)) |
|
766 |
response.find('.//{%(pec)s}Recipient' % ns).extend(message_xml.find('.//{%(pec)s}Sender' % ns)) |
|
767 | ||
768 |
response.find('.//{%(pec)s}FlowType' % ns).text = message_xml.find('.//{%(pec)s}FlowType' % ns).text |
|
769 | ||
770 |
# Strangely the same node in the response does not have the same |
|
771 |
# namespace as the node in the request, whatever... |
|
772 |
response.find('.//{%(pec)s}NumeroTeledemarche' % ns).text = message_xml.find( |
|
773 |
'.//{%(mdel)s}NumeroTeledemarche' % ns |
|
774 |
).text |
|
775 |
response.find('.//{%(pec)s}MotDePasse' % ns).text = message_xml.find( |
|
776 |
'.//{%(mdel)s}MotDePasse' % ns |
|
777 |
).text |
|
778 |
response.find('.//{%(pec)s}Etat' % ns).text = '100' |
|
779 |
response.find('.//{%(pec)s}Commentaire' % ns).text = 'Dossier transmis à la collectivité' |
|
780 |
return response |
|
781 | ||
782 |
def build_response_zip(self, fd_or_filename, etat, commentaire): |
|
783 |
with zipfile.ZipFile(fd_or_filename, 'w') as archive: |
|
784 |
message_xml = self.build_message_xml_retour(etat=etat, commentaire=commentaire) |
|
785 |
archive.writestr( |
|
786 |
'message.xml', |
|
787 |
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' |
|
788 |
+ ET.tostring(message_xml, encoding='utf-8').decode(), |
|
789 |
) |
|
790 | ||
791 |
@property |
|
792 |
def response_zip_filename(self): |
|
793 |
m = FILE_PATTERN.match(self.filename) |
|
794 | ||
795 |
numero_teledossier = m.group('identifier') |
|
796 |
code_demarche = m.group('procedure') |
|
797 |
id_enveloppe = self.id_enveloppe |
|
798 |
numero_sequence = '1' |
|
799 | ||
800 |
return '%s-%s-%s-%s.zip' % (numero_teledossier, code_demarche, id_enveloppe, numero_sequence) |
|
801 | ||
802 |
class Meta: |
|
803 |
verbose_name = _('MDEL request') |
|
804 |
verbose_name_plural = _('MDEL requests') |
|
805 |
unique_together = (('resource', 'filename'),) |
passerelle/apps/sp_fr/recensementCitoyen.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/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" %} |
|
2 | ||
3 |
{% block resource-child-breadcrumb %} |
|
4 |
{% if object.id %} |
|
5 |
<a href="#">{{ object.get_procedure_display }}</a> |
|
6 |
{% else %} |
|
7 |
<a href="#">{% trans "Add mapping" %}</a> |
|
8 |
{% endif %} |
|
9 |
{% endblock %} |
passerelle/apps/sp_fr/templates/sp_fr/mapping_form.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/resource_child_form.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% comment %} |
|
5 |
{% block resource-child-breadcrumb %} |
|
6 |
{% if object.id %} |
|
7 |
<a href="#">{{ object }}</a> |
|
8 |
{% else %} |
|
9 |
<a href="#">{% trans "Add mapping" %}</a> |
|
10 |
{% endif %} |
|
11 |
{% endblock %} |
|
12 |
{% endcomment %} |
|
13 | ||
14 |
{% block form %} |
|
15 |
{% if form.errors %} |
|
16 |
<div class="errornotice"> |
|
17 |
<p>{% trans "There were errors processing your form." %}</p> |
|
18 |
{% for error in form.non_field_errors %} |
|
19 |
<p>{{ error }}</p> |
|
20 |
{% endfor %} |
|
21 |
{% for field in form %} |
|
22 |
{% if field.is_hidden and field.errors %} |
|
23 |
<p> |
|
24 |
{% for error in field.errors %} |
|
25 |
{% blocktrans with name=field.name %}(Hidden field {{name}}) {{ error }}{% endblocktrans %} |
|
26 |
{% if not forloop.last %}<br>{% endif %} |
|
27 |
{% endfor %} |
|
28 |
</p> |
|
29 |
{% endif %} |
|
30 |
{% endfor %} |
|
31 |
</div> |
|
32 |
{% endif %} |
|
33 |
{% include "gadjo/widget.html" with field=form.procedure %} |
|
34 | ||
35 |
{% include "gadjo/widget.html" with field=form.formdef%} |
|
36 | ||
37 |
{% if form.table_fields %} |
|
38 |
<table class="main"> |
|
39 |
<thead> |
|
40 |
<tr> |
|
41 |
<td>Label</td> |
|
42 |
<td>Variable</td> |
|
43 |
</tr> |
|
44 |
</thead> |
|
45 |
<tbody> |
|
46 |
{% for field in form.table_fields %} |
|
47 |
<tr> |
|
48 |
<td>{{ field.label_tag }}</td> |
|
49 |
<td>{{ field }}</td> |
|
50 |
</tr> |
|
51 |
{% endfor %} |
|
52 |
</tbody> |
|
53 |
</table> |
|
54 |
{% endif %} |
|
55 |
{% 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 |
{% block extra-tab-buttons %} |
|
15 |
<button role="tab" aria-selected="false" aria-controls="panel-mappings" id="tab-mappings" tabindex="-1">{% trans "Mappings" %}</button> |
|
16 |
{% endblock %} |
|
17 | ||
18 |
{% block extra-tab-panels %} |
|
19 |
<div id="panel-mappings" role="tabpanel" tabindex="-1" aria-labelledby="tab-mappings" hidden> |
|
20 |
<ul> |
|
21 | ||
22 |
{% for mapping in object.mappings.all %} |
|
23 |
<li> |
|
24 |
<fieldset class="gadjo-foldable gadjo-folded" id="sp-fr-mapping-{{ mapping.pk}}"> |
|
25 |
<legend class="gadjo-foldable-widget"> |
|
26 |
<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> |
|
27 |
</legend> |
|
28 |
<div class="gadjo-folding"> |
|
29 |
{% for key, value in mapping.rules.fields.items %} |
|
30 |
{% if value %} |
|
31 |
<p>{{ value.label }} : {{ value.variable }} {% if value.expression %}({% trans "with expression" %} <tt>{{ value.expression }}</tt>){% endif %}</p> |
|
32 |
{% endif %} |
|
33 |
{% endfor %} |
|
34 |
<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> |
|
35 |
</div> |
|
36 |
</fieldset> |
|
37 |
</li> |
|
38 |
{% endfor %} |
|
39 |
</ul> |
|
40 |
<p><a class="button" href="{% url "sp-fr-mapping-new" slug=object.slug %}">{% trans "Add" %}</a></p> |
|
41 |
</div> |
|
42 |
{% 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.urls import re_path |
|
18 | ||
19 |
from . import views |
|
20 | ||
21 |
management_urlpatterns = [ |
|
22 |
re_path(r'^(?P<slug>[\w,-]+)/mapping/new/$', views.MappingNew.as_view(), name='sp-fr-mapping-new'), |
|
23 |
re_path( |
|
24 |
r'^(?P<slug>[\w,-]+)/mapping/(?P<pk>\d+)/$', views.MappingEdit.as_view(), name='sp-fr-mapping-edit' |
|
25 |
), |
|
26 |
re_path( |
|
27 |
r'^(?P<slug>[\w,-]+)/mapping/(?P<pk>\d+)/delete/$', |
|
28 |
views.MappingDelete.as_view(), |
|
29 |
name='sp-fr-mapping-delete', |
|
30 |
), |
|
31 |
re_path(r'^(?P<slug>[\w,-]+)/run/$', views.run, name='sp-fr-run'), |
|
32 |
] |
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.http import HttpResponseRedirect |
|
18 |
from django.shortcuts import get_object_or_404 |
|
19 |
from django.views.generic import CreateView, DeleteView, UpdateView |
|
20 | ||
21 |
from passerelle.base.mixins import ResourceChildViewMixin |
|
22 | ||
23 |
from . import forms, models |
|
24 | ||
25 | ||
26 |
class StayIfChanged: |
|
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().form_valid(form) |
|
33 | ||
34 |
def get_success_url(self): |
|
35 |
if self.has_changed: |
|
36 |
return self.get_changed_url() |
|
37 |
return super().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().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() |
|
67 |
return HttpResponseRedirect(resource.get_absolute_url()) |
passerelle/apps/sp_fr/xsd.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 |
import datetime |
|
18 | ||
19 |
import isodate |
|
20 |
from lxml import etree as ET |
|
21 |
from zeep.utils import qname_attr |
|
22 | ||
23 | ||
24 |
def parse_bool(boolean): |
|
25 |
return boolean.lower() == 'true' |
|
26 | ||
27 | ||
28 |
def parse_date(date): |
|
29 |
if isinstance(date, datetime.date): |
|
30 |
return date |
|
31 |
return datetime.datetime.strptime('%Y-%m-%d', date).date() |
|
32 | ||
33 | ||
34 |
XSD = 'http://www.w3.org/2001/XMLSchema' |
|
35 |
ns = {'xsd': XSD} |
|
36 | ||
37 |
SCHEMA = ET.QName(XSD, 'schema') |
|
38 |
ANNOTATION = ET.QName(XSD, 'annotation') |
|
39 |
ELEMENT = ET.QName(XSD, 'element') |
|
40 |
ATTRIBUTE = ET.QName(XSD, 'attribute') |
|
41 |
COMPLEX_TYPE = ET.QName(XSD, 'complexType') |
|
42 |
SIMPLE_TYPE = ET.QName(XSD, 'simpleType') |
|
43 |
COMPLEX_CONTENT = ET.QName(XSD, 'complexContent') |
|
44 |
EXTENSION = ET.QName(XSD, 'extension') |
|
45 |
RESTRICTION = ET.QName(XSD, 'restriction') |
|
46 |
SEQUENCE = ET.QName(XSD, 'sequence') |
|
47 |
CHOICE = ET.QName(XSD, 'choice') |
|
48 |
ALL = ET.QName(XSD, 'all') |
|
49 |
BOOLEAN = ET.QName(XSD, 'boolean') |
|
50 |
STRING = ET.QName(XSD, 'string') |
|
51 |
DATE = ET.QName(XSD, 'date') |
|
52 |
INT = ET.QName(XSD, 'int') |
|
53 |
INTEGER = ET.QName(XSD, 'integer') |
|
54 |
DATE_TIME = ET.QName(XSD, 'dateTime') |
|
55 |
ANY_TYPE = ET.QName(XSD, 'anyType') |
|
56 | ||
57 |
TYPE_CASTER = { |
|
58 |
BOOLEAN: parse_bool, |
|
59 |
STRING: str, |
|
60 |
DATE: parse_date, |
|
61 |
INT: int, |
|
62 |
INTEGER: int, |
|
63 |
DATE_TIME: isodate.parse_datetime, |
|
64 |
ANY_TYPE: lambda v: v, |
|
65 |
} |
|
66 | ||
67 | ||
68 |
class Schema: |
|
69 |
def __init__(self): |
|
70 |
self.types = {} |
|
71 |
self.elements = {} |
|
72 |
self.target_namespace = None |
|
73 |
self.element_form_default = 'qualified' |
|
74 |
self.attribute_form_default = 'unqualified' |
|
75 |
self.nsmap = {} |
|
76 | ||
77 |
def visit(self, root): |
|
78 |
assert root.tag == SCHEMA |
|
79 |
assert set(root.attrib) <= set(['targetNamespace', 'elementFormDefault', 'attributeFormDefault']), ( |
|
80 |
'unsupported schema attributes %s' % root.attrib |
|
81 |
) |
|
82 |
self.target_namespace = root.get('targetNamespace') |
|
83 |
self.element_form_default = root.get('elementFormDefault', self.element_form_default) |
|
84 |
self.attribute_form_default = root.get('attributeFormDefault', self.attribute_form_default) |
|
85 |
self.nsmap = root.nsmap |
|
86 |
self.reverse_nsmap = {value: key for key, value in self.nsmap.items()} |
|
87 | ||
88 |
# first pass |
|
89 |
for node in root: |
|
90 |
if node.tag == COMPLEX_TYPE: |
|
91 |
name = qname_attr(node, 'name') |
|
92 |
assert name, 'unsupported top complexType without name' |
|
93 |
self.types[name] = {} |
|
94 |
elif node.tag == ELEMENT: |
|
95 |
name = qname_attr(node, 'name') |
|
96 |
assert name, 'unsupported top element without name' |
|
97 |
self.elements[name] = {} |
|
98 |
elif node.tag == SIMPLE_TYPE: |
|
99 |
name = qname_attr(node, 'name') |
|
100 |
assert name, 'unsupported top simpleType without name' |
|
101 |
self.types[name] = {} |
|
102 |
else: |
|
103 |
raise NotImplementedError('unsupported top element %s' % node) |
|
104 | ||
105 |
# second pass |
|
106 |
for node in root: |
|
107 |
if node.tag == COMPLEX_TYPE: |
|
108 |
d = self.visit_complex_type(node) |
|
109 |
target = self.types |
|
110 |
elif node.tag == SIMPLE_TYPE: |
|
111 |
d = self.visit_simple_type(node) |
|
112 |
target = self.types |
|
113 |
elif node.tag == ELEMENT: |
|
114 |
d = self.visit_element(node) |
|
115 |
target = self.elements |
|
116 |
else: |
|
117 |
raise NotImplementedError |
|
118 |
if not d['name'].namespace and self.target_namespace: |
|
119 |
d['name'] = ET.QName(self.target_namespace, d['name'].localname) |
|
120 |
target[d['name']] = d |
|
121 | ||
122 |
def visit_simple_type(self, node): |
|
123 |
# ignore annotations |
|
124 |
children = [child for child in node if child.tag != ANNOTATION] |
|
125 |
d = {} |
|
126 |
name = qname_attr(node, 'name') |
|
127 |
if name: |
|
128 |
d['name'] = name |
|
129 |
assert len(children) == 1, list(node) |
|
130 |
assert children[0].tag == RESTRICTION |
|
131 |
xsd_type = qname_attr(children[0], 'base') |
|
132 |
assert xsd_type == STRING |
|
133 |
d['type'] = STRING |
|
134 |
return d |
|
135 | ||
136 |
def visit_complex_content(self, node): |
|
137 |
d = {} |
|
138 |
name = qname_attr(node, 'name') |
|
139 |
if name: |
|
140 |
d['name'] = name |
|
141 |
assert len(node) == 1 |
|
142 |
assert node[0].tag == EXTENSION |
|
143 |
xsd_type = qname_attr(node[0], 'base') |
|
144 |
d['type'] = xsd_type |
|
145 |
return d |
|
146 | ||
147 |
def visit_complex_type(self, node): |
|
148 |
# ignore annotations |
|
149 |
children = [child for child in node if child.tag != ANNOTATION] |
|
150 |
if children and children[0].tag in (SEQUENCE, CHOICE, ALL, COMPLEX_CONTENT): |
|
151 |
if children[0].tag == SEQUENCE: |
|
152 |
d = self.visit_sequence(children[0]) |
|
153 |
elif children[0].tag == CHOICE: |
|
154 |
d = self.visit_choice(children[0]) |
|
155 |
elif children[0].tag == ALL: |
|
156 |
d = self.visit_all(children[0]) |
|
157 |
elif children[0].tag == COMPLEX_CONTENT: |
|
158 |
d = self.visit_complex_content(children[0]) |
|
159 |
children = children[1:] |
|
160 |
else: |
|
161 |
d = {} |
|
162 |
for child in children: |
|
163 |
assert child.tag == ATTRIBUTE, 'unsupported complexType with child %s' % child |
|
164 |
name = qname_attr(child, 'name') |
|
165 |
assert name, 'attribute without a name %s' % ET.tostring(child) |
|
166 |
assert set(child.attrib) <= set(['use', 'type', 'name']), child.attrib |
|
167 |
attributes = d.setdefault('attributes', {}) |
|
168 |
xsd_type = qname_attr(child, 'type') |
|
169 |
attributes[name] = { |
|
170 |
'name': name, |
|
171 |
'use': child.get('use', 'optional'), |
|
172 |
'type': xsd_type, |
|
173 |
} |
|
174 | ||
175 |
name = qname_attr(node, 'name') |
|
176 |
if name: |
|
177 |
d['name'] = name |
|
178 |
return d |
|
179 | ||
180 |
def visit_element(self, node, top=False): |
|
181 |
# ignore annotations |
|
182 |
assert set(node.attrib.keys()) <= set(['name', 'type', 'minOccurs', 'maxOccurs']), node.attrib |
|
183 |
children = [child for child in node if child.tag != ANNOTATION] |
|
184 |
# we handle elements with a name and one child, an anonymous complex type |
|
185 |
# or element without children referencing a complex type |
|
186 |
name = qname_attr(node, 'name') |
|
187 |
assert name is not None |
|
188 |
min_occurs = node.attrib.get('minOccurs') or 1 |
|
189 |
max_occurs = node.attrib.get('maxOccurs') or 1 |
|
190 |
d = { |
|
191 |
'name': name, |
|
192 |
'min_occurs': int(min_occurs), |
|
193 |
'max_occurs': max_occurs if max_occurs == 'unbounded' else int(max_occurs), |
|
194 |
} |
|
195 |
if len(children) == 1: |
|
196 |
ctype_node = children[0] |
|
197 |
assert ctype_node.tag == COMPLEX_TYPE |
|
198 |
assert ctype_node.attrib == {} |
|
199 |
d.update(self.visit_complex_type(ctype_node)) |
|
200 |
return d |
|
201 |
elif len(children) == 0: |
|
202 |
xsd_type = qname_attr(node, 'type') |
|
203 |
if xsd_type is None: |
|
204 |
xsd_type = STRING |
|
205 |
d['type'] = xsd_type |
|
206 |
return d |
|
207 |
else: |
|
208 |
raise NotImplementedError('unsupported element with more than one children %s' % list(node)) |
|
209 | ||
210 |
def visit_sequence(self, node): |
|
211 |
assert set(node.attrib) <= set(['maxOccurs']), node.attrib |
|
212 |
sequence = [] |
|
213 | ||
214 |
for element_node in node: |
|
215 |
assert element_node.tag in ( |
|
216 |
ELEMENT, |
|
217 |
CHOICE, |
|
218 |
), 'unsupported sequence with child not an element or a choice %s' % ET.tostring(element_node) |
|
219 |
if element_node.tag == ELEMENT: |
|
220 |
sequence.append(self.visit_element(element_node)) |
|
221 |
elif element_node.tag == CHOICE: |
|
222 |
sequence.append(self.visit_choice(element_node)) |
|
223 | ||
224 |
d = { |
|
225 |
'sequence': sequence, |
|
226 |
} |
|
227 |
if 'maxOccurs' in node.attrib: |
|
228 |
d['max_occurs'] = node.get('maxOccurs', 1) |
|
229 |
return d |
|
230 | ||
231 |
def visit_all(self, node): |
|
232 |
return self.visit_sequence(node) |
|
233 | ||
234 |
def visit_choice(self, node): |
|
235 |
assert node.attrib == {}, 'unsupported choice with attributes %s' % node.attrib |
|
236 |
choice = [] |
|
237 | ||
238 |
for element_node in node: |
|
239 |
assert element_node.tag == ELEMENT, 'unsupported sequence with child not an element %s' % node |
|
240 |
choice.append(self.visit_element(element_node)) |
|
241 | ||
242 |
return {'choice': choice} |
|
243 | ||
244 |
def qname_display(self, name): |
|
245 |
if name.namespace in self.reverse_nsmap: |
|
246 |
name = '%s:%s' % (self.reverse_nsmap[name.namespace], name.localname) |
|
247 |
return str(name) |
|
248 | ||
249 |
def paths(self): |
|
250 |
roots = sorted(self.elements.keys()) |
|
251 | ||
252 |
def helper(path, ctype, is_type=False): |
|
253 |
name = None |
|
254 |
if 'name' in ctype: |
|
255 |
name = ctype['name'] |
|
256 |
max_occurs = ctype.get('max_occurs', 1) |
|
257 |
max_occurs = 3 if max_occurs == 'unbounded' else max_occurs |
|
258 |
if 'type' in ctype: |
|
259 |
if name and not is_type: |
|
260 |
path = path + [name] |
|
261 |
xsd_type = ctype['type'] |
|
262 |
if xsd_type in self.types: |
|
263 |
sub_type = self.types[xsd_type] |
|
264 |
for subpath in helper(path, sub_type, is_type=True): |
|
265 |
yield subpath |
|
266 |
else: |
|
267 |
if max_occurs > 1: |
|
268 |
for i in range(max_occurs): |
|
269 |
yield path[:-1] + [ |
|
270 |
ET.QName(name.namespace, name.localname + '_%d' % (i + 1)) |
|
271 |
], xsd_type |
|
272 |
yield path, xsd_type |
|
273 |
else: |
|
274 |
for extension in ( |
|
275 |
[''] if max_occurs == 1 else [''] + ['_%s' % i for i in list(range(1, max_occurs + 1))] |
|
276 |
): |
|
277 |
new_path = path |
|
278 |
if name and not is_type: |
|
279 |
new_path = new_path + [ET.QName(name.namespace, name.localname + extension)] |
|
280 |
if 'sequence' in ctype: |
|
281 |
for sub_ctype in ctype['sequence']: |
|
282 |
for subpath in helper(new_path, sub_ctype): |
|
283 |
yield subpath |
|
284 |
elif 'choice' in ctype: |
|
285 |
for sub_ctype in ctype['choice']: |
|
286 |
for subpath in helper(new_path, sub_ctype): |
|
287 |
yield subpath |
|
288 | ||
289 |
for root in roots: |
|
290 |
for path in helper([], self.elements[root]): |
|
291 |
yield path |
|
292 | ||
293 | ||
294 |
class Path: |
|
295 |
def __init__(self, path, xsd_type): |
|
296 |
assert path |
|
297 |
self.path = path |
|
298 |
self.xsd_type = xsd_type |
|
299 |
try: |
|
300 |
self.caster = TYPE_CASTER[xsd_type] |
|
301 |
except KeyError: |
|
302 |
raise KeyError(str(xsd_type)) |
|
303 | ||
304 |
def resolve(self, root): |
|
305 |
def helper(node, path): |
|
306 |
if not path: |
|
307 |
return node |
|
308 |
else: |
|
309 |
for child in node: |
|
310 |
if child.tag == path[0]: |
|
311 |
return helper(child, path[1:]) |
|
312 | ||
313 |
if root.tag != self.path[0]: |
|
314 |
return None |
|
315 |
child = helper(root, self.path[1:]) |
|
316 |
if child is not None and child.text and not list(child): |
|
317 |
return self.caster(child.text) |
|
318 | ||
319 |
def __str__(self): |
|
320 |
return '.'.join(str(name) for name in self.path) |
passerelle/settings.py | ||
---|---|---|
169 | 169 |
'passerelle.apps.sivin', |
170 | 170 |
'passerelle.apps.soap', |
171 | 171 |
'passerelle.apps.solis', |
172 |
'passerelle.apps.sp_fr', |
|
173 | 172 |
'passerelle.apps.twilio', |
174 | 173 |
'passerelle.apps.vivaticket', |
175 | 174 |
# backoffice templates and static |
tests/wcs/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 mock |
|
19 |
import pytest |
|
20 | ||
21 |
import tests.utils |
|
22 |
from passerelle.apps.sp_fr.models import Resource |
|
23 |
from passerelle.utils.sftp import SFTP |
|
24 |
from passerelle.utils.wcs import FormDefRef, get_wcs_choices |
|
25 | ||
26 |
DUMMY_CONTENT = { |
|
27 |
'DILA': { |
|
28 |
'a.zip': 'a', |
|
29 |
} |
|
30 |
} |
|
31 | ||
32 | ||
33 |
@pytest.fixture |
|
34 |
def spfr(settings, wcs_host, db, sftpserver): |
|
35 |
wcs_host.add_api_secret('test', 'test') |
|
36 |
settings.KNOWN_SERVICES = { |
|
37 |
'wcs': { |
|
38 |
'eservices': { |
|
39 |
'title': 'Démarches', |
|
40 |
'url': wcs_host.url, |
|
41 |
'secret': 'test', |
|
42 |
'orig': 'test', |
|
43 |
} |
|
44 |
} |
|
45 |
} |
|
46 |
yield tests.utils.make_resource( |
|
47 |
Resource, |
|
48 |
title='Test 1', |
|
49 |
slug='test1', |
|
50 |
description='Connecteur de test', |
|
51 |
input_sftp=SFTP('sftp://john:doe@{server.host}:{server.port}/DILA/'.format(server=sftpserver)), |
|
52 |
output_sftp=SFTP('sftp://john:doe@{server.host}:{server.port}/DILA/'.format(server=sftpserver)), |
|
53 |
) |
|
54 | ||
55 | ||
56 |
def test_resource(spfr): |
|
57 |
assert [x[1] for x in get_wcs_choices()] == ['---------', 'D\xe9marches - Demande'] |
|
58 | ||
59 | ||
60 |
def test_sftp_access(spfr, sftpserver): |
|
61 |
with sftpserver.serve_content(DUMMY_CONTENT): |
|
62 |
with spfr.input_sftp.client() as input_sftp: |
|
63 |
assert input_sftp.listdir() == ['a.zip'] |
|
64 |
with spfr.output_sftp.client() as output_sftp: |
|
65 |
assert output_sftp.listdir() == ['a.zip'] |
|
66 | ||
67 | ||
68 |
def test_import_export(spfr): |
|
69 |
# mock FormDefRef.formdef property to prevent w.c.s. API calls |
|
70 |
with mock.patch.object(FormDefRef, 'formdef') as mock_formdef: |
|
71 |
mock_formdef.__get__ = mock.Mock(return_value=None) |
|
72 |
mapping = spfr.mappings.create(procedure='DOC', formdef=FormDefRef('wcs:formdef1'), rules={'a': 'b'}) |
|
73 |
serialization = spfr.export_json() |
|
74 |
spfr.delete() |
|
75 |
new_spfr = spfr.__class__.import_json(serialization) |
|
76 |
assert dict(spfr.__dict__, _state=None, id=None, logger=None) == dict( |
|
77 |
new_spfr.__dict__, id=None, logger=None, _state=None |
|
78 |
) |
|
79 |
assert dict( |
|
80 |
new_spfr.mappings.get().__dict__, _resource_cache=None, resource_id=None, id=None, _state=None |
|
81 |
) == dict(mapping.__dict__, _resource_cache=None, resource_id=None, id=None, _state=None) |
|
82 |
- |