Projet

Général

Profil

0011-initialize-sp_fr-connector-31595.patch

Benjamin Dauvergne, 19 avril 2019 14:59

Télécharger (66,9 ko)

Voir les différences:

Subject: [PATCH 11/11] initialize sp_fr connector (#31595)

New connector for transfering forms from Service-Public.fr to w.c.s.
 passerelle/apps/sp_fr/DDPACS.XSD              | 129 ++++
 passerelle/apps/sp_fr/DOC.XSD                 | 108 +++
 passerelle/apps/sp_fr/RCO.XSD                 | 137 ++++
 passerelle/apps/sp_fr/__init__.py             |   0
 passerelle/apps/sp_fr/admin.py                |  27 +
 passerelle/apps/sp_fr/fields.py               |  94 +++
 passerelle/apps/sp_fr/forms.py                |  69 ++
 passerelle/apps/sp_fr/models.py               | 652 ++++++++++++++++++
 .../variable_and_expression_widget.html       |   6 +
 .../sp_fr/mapping_confirm_delete.html         |   9 +
 .../sp_fr/templates/sp_fr/mapping_form.html   |  55 ++
 .../templates/sp_fr/resource_detail.html      |  69 ++
 passerelle/apps/sp_fr/urls.py                 |  30 +
 passerelle/apps/sp_fr/views.py                |  67 ++
 passerelle/settings.py                        |   1 +
 passerelle/static/css/style.css               |   6 +
 tests/test_sp_fr.py                           |  65 ++
 17 files changed, 1524 insertions(+)
 create mode 100644 passerelle/apps/sp_fr/DDPACS.XSD
 create mode 100644 passerelle/apps/sp_fr/DOC.XSD
 create mode 100644 passerelle/apps/sp_fr/RCO.XSD
 create mode 100644 passerelle/apps/sp_fr/__init__.py
 create mode 100644 passerelle/apps/sp_fr/admin.py
 create mode 100644 passerelle/apps/sp_fr/fields.py
 create mode 100644 passerelle/apps/sp_fr/forms.py
 create mode 100644 passerelle/apps/sp_fr/models.py
 create mode 100644 passerelle/apps/sp_fr/templates/passerelle/widgets/variable_and_expression_widget.html
 create mode 100644 passerelle/apps/sp_fr/templates/sp_fr/mapping_confirm_delete.html
 create mode 100644 passerelle/apps/sp_fr/templates/sp_fr/mapping_form.html
 create mode 100644 passerelle/apps/sp_fr/templates/sp_fr/resource_detail.html
 create mode 100644 passerelle/apps/sp_fr/urls.py
 create mode 100644 passerelle/apps/sp_fr/views.py
 create mode 100644 tests/test_sp_fr.py
passerelle/apps/sp_fr/DDPACS.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/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/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

  
19
from .models import Request
20

  
21

  
22
class RequestAdmin(admin.ModelAdmin):
23
    data_hierarchy = 'created'
24
    search_fields = ['url', 'filename']
25
    list_display = ['id', 'created', 'modified', 'state', 'filename', 'url']
26

  
27
admin.site.register(Request, RequestAdmin)
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.core.exceptions import ValidationError
18
from django.template import engines, TemplateSyntaxError
19
from django import forms
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(VariableAndExpressionWidget, self).__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__(self, choices=(), required=True, widget=None, label=None,
55
                 initial=None, help_text='', *args, **kwargs):
56
        fields = [
57
            forms.ChoiceField(choices=choices, required=required),
58
            forms.CharField(required=False, validators=[validate_django_template]),
59
        ]
60
        super(VariableAndExpressionField, self).__init__(
61
            fields=fields,
62
            required=required,
63
            widget=widget,
64
            label=label,
65
            initial=initial,
66
            help_text=help_text,
67
            require_all_fields=False, *args, **kwargs)
68
        self.choices = choices
69

  
70
    def _get_choices(self):
71
        return self._choices
72

  
73
    def _set_choices(self, value):
74
        # Setting choices also sets the choices on the widget.
75
        # choices can be any iterable, but we call list() on it because
76
        # it will be consumed more than once.
77
        if callable(value):
78
            value = forms.CallableChoiceIterator(value)
79
        else:
80
            value = list(value)
81
        self._choices = value
82
        self.widget.widgets[0].choices = value
83
    choices = property(_get_choices, _set_choices)
84

  
85
    def compress(self, data):
86
        try:
87
            variable, expression = data
88
        except (ValueError, TypeError):
89
            return None
90
        else:
91
            return {
92
                'variable': variable,
93
                'expression': expression,
94
            }
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 base64
24
import datetime
25
import unicodedata
26

  
27
from lxml import etree as ET
28

  
29
from django.core.urlresolvers import reverse
30
from django.core.files import File
31
from django.db import models, transaction
32
from django.template import engines
33
from django.utils import six
34
from django.utils.translation import ugettext_lazy as _, ugettext
35

  
36
from jsonfield import JSONField
37

  
38
from passerelle.base.models import BaseResource
39
from passerelle.utils.api import endpoint
40
from passerelle.utils.sftp import SFTPField
41
from passerelle.utils.wcs import FormDefField, get_wcs_choices
42
from passerelle.utils.xsd import Schema
43
from passerelle.utils.xml import text_content
44

  
45

  
46
PROCEDURE_DOC = 'DOC'
47
PROCEDURE_RCO = 'RCO'
48
PROCEDURE_DDPACS = 'DDPACS'
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-Z]+)-(?P<sequence>\d+).zip$')
56
ENT_PATTERN = re.compile(r'^.*-ent-\d+(?:-.*)?.xml$')
57
NSMAP = {
58
    'dgme-metier': 'http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier'
59
}
60
ROUTAGE_XPATH = ET.XPath(
61
    ('dgme-metier:Routage/dgme-metier:Donnee/dgme-metier:Valeur/text()'),
62
    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('dgme-metier', 'http://finances.gouv.fr/dgme/gf/composants/teledemarchexml/donnee/metier')
71

  
72

  
73
def simplify(s):
74
    '''Simplify XML node tag names because XSD from DGME are garbage'''
75
    if not s:
76
        return ''
77
    if not isinstance(s, six.text_type):
78
        s = six.text_type(s, 'utf-8', 'ignore')
79
    s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore')
80
    s = re.sub(r'[^\w\s\'-_]', '', s)
81
    s = s.replace('-', '_')
82
    s = re.sub(r'[\s\']+', '', s)
83
    return s.strip().lower()
84

  
85

  
86
class Resource(BaseResource):
87
    category = _('Business Process Connectors')
88

  
89
    input_sftp = SFTPField(
90
        verbose_name=_('Input SFTP URL'),
91
        null=True)
92

  
93
    output_sftp = SFTPField(
94
        verbose_name=_('Output SFTP URL'),
95
        null=True)
96

  
97
    def check_status(self):
98
        with self.input_sftp.client() as sftp:
99
            sftp.listdir()
100
        with self.output_sftp.client() as sftp:
101
            sftp.listdir()
102
        get_wcs_choices(session=self.requests)
103

  
104
    @endpoint(name='ping', description=_('Check Solis API availability'))
105
    def ping(self, request):
106
        self.check_status()
107
        return {'err': 0}
108

  
109
    def run_loop(self, count=1):
110
        with transaction.atomic():
111
            # lock resource
112
            r = Resource.objects.select_for_update(skip_locked=True).filter(pk=self.pk)
113
            if not r:
114
                # already locked
115
                self.logger.info('did nothing')
116
                return
117
            with self.input_sftp.client() as sftp:
118
                try:
119
                    sftp.lstat('DONE')
120
                except IOError:
121
                    sftp.mkdir('DONE')
122

  
123
                try:
124
                    sftp.lstat('FAILED')
125
                except IOError:
126
                    sftp.mkdir('FAILED')
127

  
128
                def helper():
129
                    for file_stat in sftp.listdir_attr():
130
                        if stat.S_ISDIR(file_stat.st_mode):
131
                            continue
132
                        yield file_stat.filename
133

  
134
                for filename, i in zip(helper(), range(count)):
135
                    m = FILE_PATTERN.match(filename)
136
                    if not m:
137
                        self.logger.info('file "%s" did not match pattern %s, moving to FAILED/',
138
                                         filename, FILE_PATTERN)
139
                        sftp.rename(filename, 'FAILED/' + filename)
140
                        continue
141
                    procedure = m.group('procedure')
142
                    try:
143
                        mapping = self.mappings.get(procedure=procedure)
144
                    except Mapping.DoesNotExist:
145
                        self.logger.info('no mapping for procedure "%s" for file "%s", moving to FAILED/',
146
                                         procedure, filename)
147
                        continue
148

  
149
                    handler = self.FileHandler(
150
                        resource=self,
151
                        sftp=sftp,
152
                        filename=filename,
153
                        identifier=m.group('identifier'),
154
                        procedure=procedure,
155
                        sequence=m.group('sequence'),
156
                        mapping=mapping)
157
                    try:
158
                        move, error = handler()
159
                    except Exception:
160
                        self.logger.exception('handling of file "%s" failed', filename)
161
                        # sftp.rename(filename, 'FAILED/' + filename)
162
                    else:
163
                        if move and error:
164
                            self.logger.error('handling of file "%s" failed: %s', filename, error)
165
                            # sftp.rename(filename, 'FAILED/' + filename)
166
                        else:
167
                            if error:
168
                                self.logger.warning('handling of file "%s" failed: %s', filename, error)
169
                            elif move:
170
                                sftp.rename(filename, 'DONE/' + filename)
171

  
172
    class FileHandler(object):
173
        def __init__(self, resource, sftp, filename, identifier, procedure, sequence, mapping):
174
            self.resource = resource
175
            self.sftp = sftp
176
            self.filename = filename
177
            self.identifier = identifier
178
            self.procedure = procedure
179
            self.sequence = sequence
180
            self.mapping = mapping
181
            self.variables = list(self.mapping.variables)
182
            self.request = Request.objects.filter(resource=resource, filename=filename).first()
183

  
184
        def __call__(self):
185
            if not self.request:
186
                with self.sftp.open(self.filename) as fd:
187
                    with transaction.atomic():
188
                        self.request = Request.objects.create(
189
                            resource=self.resource,
190
                            filename=self.filename)
191
                        self.request.state = Request.STATE_RECEIVED
192
                        self.request.archive.save(self.filename, File(fd))
193
            if self.request.state == Request.STATE_RECEIVED:
194
                with self.request.archive as fd:
195
                    # error during processing are fatal, we want to log them
196
                    data, error = self.process(fd)
197
                if not data:
198
                    return False, error
199
                try:
200
                    backoffice_url = self.transfer(data)
201
                except Exception as e:
202
                    raise
203
                    return False, 'error during transfer to w.c.s %s' % e
204
                self.request.url = backoffice_url
205
                self.request.state = Request.STATE_TRANSFERED
206
                self.request.save()
207

  
208
            if self.request.state == Request.STATE_TRANSFERED:
209
                try:
210
                    self.response()
211
                except Exception as e:
212
                    return False, 'error during response to service-public.fr %s' % e
213
                self.request.state = Request.STATE_RETURNED
214
                self.request.save()
215

  
216
        def process(self, fd):
217
            try:
218
                archive = zipfile.ZipFile(fd)
219
            except Exception:
220
                return False, 'could not load zipfile'
221
            # sort files
222
            doc_files = []
223
            ent_files = []
224
            attachments = {}
225
            for name in archive.namelist():
226
                if ENT_PATTERN.match(name):
227
                    ent_files.append(name)
228

  
229
            if len(ent_files) != 1:
230
                return False, 'too many/few ent files found: %s' % ent_files
231

  
232
            ent_file = ent_files[0]
233

  
234
            with archive.open(ent_file) as fd:
235
                document = ET.parse(fd)
236

  
237
            for pj_node in PIECE_JOINTE_XPATH(document):
238
                code = CODE_XPATH(pj_node)[0].text
239
                code = 'pj_' + code.lower().replace('-', '_')
240
                fichier = FICHIER_XPATH(pj_node)[0].text
241
                attachments.setdefault(code, []).append(fichier)
242
            for doc_node in DOCUMENTS_XPATH(document):
243
                code = CODE_XPATH(doc_node)[0].text
244
                code = 'doc_' + code.lower().replace('-', '_')
245
                fichier = FICHIER_DONNEES_XPATH(doc_node)[0].text
246
                attachments.setdefault(code, []).append(fichier)
247

  
248
            doc_files = [value for l in attachments.values() for value in l if value.lower().endswith('.xml')]
249
            if len(doc_files) != 1:
250
                return False, 'too many/few doc files found: %s' % doc_files
251

  
252
            for key in attachments:
253
                if len(attachments[key]) > 1:
254
                    return False, 'too many attachments of kind %s: %r' % (key, attachments[key])
255
                name = attachments[key][0]
256
                with archive.open(attachments[key][0]) as zip_fd:
257
                    content = zip_fd.read()
258
                attachments[key] = {
259
                    'filename': name,
260
                    'content': base64.b64encode(content).decode('ascii'),
261
                    'content_type': 'application/octet-stream',
262
                }
263

  
264
            if self.procedure == 'RCO' and not attachments:
265
                return False, 'no attachments but RCO requires them'
266

  
267
            doc_file = doc_files[0]
268

  
269
            insee_codes = ROUTAGE_XPATH(document)
270
            if len(insee_codes) != 1:
271
                return False, 'too many/few insee codes found: %s' % insee_codes
272
            insee_code = insee_codes[0]
273

  
274
            data = {'insee_code': insee_code}
275
            data.update(attachments)
276

  
277
            with archive.open(doc_file) as fd:
278
                document = ET.parse(fd)
279
                data.update(self.extract_data(document))
280
                if hasattr(self, 'update_data_%s' % self.procedure):
281
                    getattr(self, 'update_data_%s' % self.procedure)(data)
282
            return data, None
283

  
284
        def transfer(self, data):
285
            formdef = self.mapping.formdef
286
            formdef.session = self.resource.requests
287

  
288
            with formdef.submit() as submitter:
289
                submitter.submission_channel = 'web'
290
                submitter.submission_context = {
291
                    'mdel_procedure': self.procedure,
292
                    'mdel_identifier': self.identifier,
293
                    'mdel_sequence': self.sequence,
294
                }
295
                fields = self.mapping.rules.get('fields', {})
296
                for name in fields:
297
                    field = fields[name]
298
                    variable = field['variable']
299
                    expression = field['expression']
300
                    value = data.get(variable)
301
                    if expression.strip():
302
                        template = engines['django'].from_string(expression)
303
                        context = data.copy()
304
                        context['value'] = value
305
                        value = template.render(context)
306
                    submitter.set(name, value)
307
            return submitter.result.backoffice_url
308

  
309
        def response(self):
310
            raise NotImplementedError
311

  
312
        def get_data(self, data, name):
313
            # prevent error in manual mapping
314
            assert name in self.variables, 'variable "%s" is unknown' % name
315
            return data.get(name, '')
316

  
317
        def update_data_DOC(self, data):
318
            def get(name):
319
                return self.get_data(data, name)
320

  
321
            numero_permis_construire = get('doc_declarant_designation_permis_numero_permis_construire')
322
            numero_permis_amenager = get('doc_declarant_designation_permis_numero_permis_amenager')
323
            data['type_permis'] = u'Un permis de construire' if numero_permis_construire else u'Un permis d\'aménager'
324
            data['numero_permis'] = numero_permis_construire or numero_permis_amenager
325
            particulier = get('doc_declarant_identite_type_personne').strip().lower() == 'true'
326
            data['type_declarant'] = u'Un particulier' if particulier else u'Une personne morale'
327
            if particulier:
328
                data['nom'] = get('doc_declarant_identite_personne_physique_nom')
329
                data['prenoms'] = get('doc_declarant_identite_personne_physique_prenom')
330
            else:
331
                data['nom'] = get('doc_declarant_identite_personne_morale_representant_personne_morale_nom')
332
                data['prenoms'] = get('doc_declarant_identite_personne_morale_representant_personne_morale_prenom')
333
            mapping = {
334
                '1000': 'Monsieur',
335
                '1001': 'Madame',
336
                '1002': 'Madame et Monsieur',
337
            }
338
            if particulier:
339
                data['civilite_particulier'] = mapping.get(get('doc_declarant_identite_personne_physique_civilite'), '')
340
            else:
341
                data['civilite_pm'] = mapping.get(
342
                    get('doc_declarant_identite_personne_morale_representant_personne_morale_civilite'), '')
343
            data['portee'] = (u'Pour la totalité des travaux'
344
                              if get('doc_ouverture_chantier_totalite_travaux').lower().strip() == 'true'
345
                              else u'Pour une tranche des travaux')
346

  
347
        def update_data_RCO(self, data):
348
            def get(name):
349
                return self.get_data(data, name)
350

  
351
            motif = (
352
                get('recensementcitoyen_formalite_formalitemotifcode_1')
353
                or get('recensementcitoyen_formalite_formalitemotifcode_2')
354
            )
355
            data['motif'] = {
356
                'RECENSEMENT': '1',
357
                'EXEMPTION': '2'
358
            }[motif]
359
            if data['motif'] == '2':
360
                data['motif_exempte'] = (
361
                    u"Titulaire d'une carte d'invalidité de 80% minimum"
362
                    if get('recensementcitoyen_formalite_formalitemotifcode_2') == 'INFIRME'
363
                    else u"Autre situation")
364
            data['justificatif_exemption'] = get('pj_je')
365
            data['double_nationalite'] = (
366
                'Oui'
367
                if get('recensementcitoyen_personne_nationalite')
368
                else 'Non')
369
            data['residence_differente'] = (
370
                'Oui'
371
                if get('recensementcitoyen_personne_adresseresidence_localite')
372
                else 'Non')
373
            data['civilite'] = (
374
                'Monsieur'
375
                if get('recensementcitoyen_personne_civilite') == 'M'
376
                else 'Madame'
377
            )
378

  
379
            def get_lieu_naissance(variable, code):
380
                for idx in ['', '_1', '_2']:
381
                    v = variable + idx
382
                    if get(v + '_code') == code:
383
                        return get(v + '_nom')
384

  
385
            data['cp_naissance'] = get_lieu_naissance('recensementcitoyen_personne_lieunaissance', 'AUTRE')
386
            data['commune_naissance'] = get_lieu_naissance('recensementcitoyen_personne_lieunaissance', 'COMMUNE')
387
            data['justificatif_identite'] = get('pj_ji')
388
            situation_matrimoniale = get('recensementcitoyen_personne_situationfamille_situationmatrimoniale')
389
            data['situation_familiale'] = {
390
                u'Célibataire': u'Célibataire',
391
                u'Marié': u'Marié(e)',
392
            }.get(situation_matrimoniale, u'Autres')
393
            if data['situation_familiale'] == u'Autres':
394
                data['situation_familiale_precision'] = situation_matrimoniale
395
            pupille = get('recensementcitoyen_personne_situationfamille_pupille')
396
            data['pupille'] = (
397
                'Oui'
398
                if pupille
399
                else 'Non'
400
            )
401
            data['pupille_categorie'] = {
402
                'NATION': u"Pupille de la nation",
403
                'ETAT': u"Pupille de l'État",
404
            }.get(pupille)
405
            for idx in ['', '_1', '_2']:
406
                code = get('recensementcitoyen_personne_methodecontact%s_canalcode' % idx)
407
                uri = get('recensementcitoyen_personne_methodecontact%s_uri' % idx)
408
                if code == 'EMAIL':
409
                    data['courriel'] = uri
410
                if code == 'TEL':
411
                    data['telephone_fixe'] = uri
412
            data['justificatif_famille'] = get('pj_jf')
413
            data['filiation_inconnue_p1'] = not get('recensementcitoyen_filiationpere_nomfamille')
414
            data['filiation_inconnue_p2'] = not get('recensementcitoyen_filiationmere_nomfamille')
415
            data['cp_naissance_p1'] = get_lieu_naissance('recensementcitoyen_filiationpere_lieunaissance', 'AUTRE')
416
            data['cp_naissance_p2'] = get_lieu_naissance('recensementcitoyen_filiationmere_lieunaissance', 'AUTRE')
417
            data['commune_naissance_p1'] = get_lieu_naissance(
418
                'recensementcitoyen_filiationpere_lieunaissance', 'COMMUNE')
419
            data['commune_naissance_p2'] = get_lieu_naissance(
420
                'recensementcitoyen_filiationmere_lieunaissance', 'COMMUNE')
421
            for key in data:
422
                if key.endswith('_datenaissance') and data[key]:
423
                    data[key] = (
424
                        datetime.datetime.strptime(data[key], '%d/%m/%Y')
425
                        .date()
426
                        .strftime('%Y-%m-%d')
427
                    )
428

  
429
        def update_data_DDPACS(self, data):
430
            def get(name):
431
                return self.get_data(data, name)
432

  
433
            civilite_p1 = get('pacs_partenaire1_civilite')
434
            data['civilite_p1'] = 'Monsieur' if civilite_p1 == 'M' else 'Madame'
435
            data['acte_naissance_p1'] = get('pj_an')
436
            data['identite_verifiee_p1'] = 'Oui' if get('pacs_partenaire1_titreidentiteverifie') == 'true' else 'None'
437

  
438
            civilite_p2 = get('pacs_partenaire2_civilite')
439
            data['civilite_p2'] = 'Monsieur' if civilite_p2 == 'M' else 'Madame'
440
            data['acte_naissance_p2'] = get('pj_anp')
441
            data['identite_verifiee_p2'] = 'Oui' if get('pacs_partenaire2_titreidentiteverifie') == 'true' else 'None'
442

  
443
            data['type_convention'] = '2' if get('pacs_convention_conventionspecifique') == 'true' else '1'
444
            data['aide_materielle'] = (
445
                '1' if get('pacs_convention_conventiontype_aidemateriel_typeaidemateriel') == 'aideProportionnel'
446
                else '2')
447
            data['regime'] = '1' if get('pacs_convention_conventiontype_regimepacs') == 'legal' else '2'
448
            data['convention_specifique'] = get('pj_cp')
449

  
450
        def extract_data(self, document):
451
            '''Convert XML into a dictionnary of values'''
452
            root = document.getroot()
453

  
454
            def tag_name(node):
455
                return simplify(ET.QName(node.tag).localname)
456

  
457
            def helper(path, node):
458
                if len(node):
459
                    tags = collections.Counter(tag_name(child) for child in node)
460
                    counter = collections.Counter()
461
                    for child in node:
462
                        name = tag_name(child)
463
                        if tags[name] > 1:
464
                            counter[name] += 1
465
                            name += '_%s' % counter[name]
466
                        for p, value in helper(path + [name], child):
467
                            yield p, value
468
                else:
469
                    yield path, text_content(node)
470
            return {'_'.join(path): value for path, value in helper([tag_name(root)], root)}
471

  
472
    class Meta:
473
        verbose_name = _('Service-Public.fr')
474

  
475

  
476
class SPFRMessage(object):
477
    @classmethod
478
    def from_file(self, resource, filename, fd):
479
        m = re.match(r'(.*)-([A-Z]+)-(\d+).zip', filename)
480
        if not m:
481
            resource.logger.warning('found file with an unknown pattern %s moving in DONE/', filename)
482
            return None
483
        mdel_number, procedure, sequence = m.groups()
484

  
485

  
486
def default_rule():
487
    return {}
488

  
489

  
490
@six.python_2_unicode_compatible
491
class Mapping(models.Model):
492
    resource = models.ForeignKey(
493
        Resource,
494
        verbose_name=_('Resource'),
495
        related_name='mappings')
496

  
497
    procedure = models.CharField(
498
        verbose_name=_('Procedure'),
499
        choices=PROCEDURES,
500
        unique=True,
501
        max_length=8)
502

  
503
    formdef = FormDefField(
504
        verbose_name=_('Formdef'))
505

  
506
    rules = JSONField(
507
        verbose_name=_('Rules'),
508
        default=default_rule)
509

  
510
    def get_absolute_url(self):
511
        return reverse('sp-fr-mapping-edit', kwargs=dict(
512
            slug=self.resource.slug,
513
            pk=self.pk))
514

  
515
    @property
516
    def xsd(self):
517
        doc = ET.parse(os.path.join(os.path.dirname(__file__), '%s.XSD' % self.procedure))
518
        schema = Schema()
519
        schema.visit(doc.getroot())
520
        return schema
521

  
522
    @property
523
    def variables(self):
524
        yield 'insee_code'
525
        for path, xsd_type in self.xsd.paths():
526
            names = [simplify(tag.localname) for tag in path]
527
            yield '_'.join(names)
528
        if hasattr(self, 'variables_%s' % self.procedure):
529
            for variable in getattr(self, 'variables_%s' % self.procedure):
530
                yield variable
531

  
532
    @property
533
    def variables_DOC(self):
534
        yield 'type_permis'
535
        yield 'numero_permis'
536
        yield 'type_declarant'
537
        yield 'nom'
538
        yield 'prenoms'
539
        yield 'civilite_particulier'
540
        yield 'civilite_pm'
541
        yield 'portee'
542

  
543
    @property
544
    def variables_RCO(self):
545
        yield 'motif'
546
        yield 'motif_exemple'
547
        yield 'justificatif_exemption'
548
        yield 'double_nationalite'
549
        yield 'residence_differente'
550
        yield 'civilite'
551
        yield 'cp_naissance'
552
        yield 'commune_naissance'
553
        yield 'pj_je'
554
        yield 'pj_ji'
555
        yield 'situation_familiale'
556
        yield 'situation_familiale_precision'
557
        yield 'pupille'
558
        yield 'pupille_categorie'
559
        yield 'courriel'
560
        yield 'telephone_fixe'
561
        yield 'pj_jf'
562
        yield 'filiation_inconnue_p1'
563
        yield 'filiation_inconnue_p2'
564
        yield 'cp_naissance_p1'
565
        yield 'cp_naissance_p2'
566
        yield 'commune_naissance_p1'
567
        yield 'commune_naissance_p2'
568

  
569
    @property
570
    def variables_DDPACS(self):
571
        yield 'pj_an'
572
        yield 'pj_anp'
573
        yield 'pj_cp'
574
        yield 'doc_15725_01'
575
        yield 'doc_flux_pacs'
576
        yield 'doc_recappdf'
577
        yield 'civilite_p1'
578
        yield 'acte_naissance_p1'
579
        yield 'identite_verifiee_p1'
580

  
581
        yield 'civilite_p2'
582
        yield 'acte_naissance_p2'
583
        yield 'identite_verifiee_p2'
584

  
585
        yield 'type_convention'
586
        yield 'aide_materielle'
587
        yield 'regime'
588
        yield 'convention_specifique'
589

  
590
    def __str__(self):
591
        return ugettext('Mapping from "{procedure}" to formdef "{formdef}"').format(
592
            procedure=self.get_procedure_display(),
593
            formdef=self.formdef.title if self.formdef else '-')
594

  
595
    class Meta:
596
        verbose_name = _('MDEL mapping')
597
        verbose_name_plural = _('MDEL mappings')
598

  
599

  
600
class Request(models.Model):
601
    # To prevent mixing errors from analysing archive from s-p.fr and errors
602
    # from pushing to w.c.s we separate processing with three steps:
603
    # - receiving, i.e. copying zipfile from SFTP and storing them locally
604
    # - processing, i.e. openeing the zipfile and extracting content as we need it
605
    # - transferring, pushing content as a new form in w.c.s.
606
    STATE_RECEIVED = 'received'
607
    STATE_TRANSFERED = 'transfered'
608
    STATE_RETURNED = 'returned'
609
    STATE_ERROR = 'error'
610
    STATES = [
611
        (STATE_RECEIVED, _('Received')),
612
        (STATE_TRANSFERED, _('Transfered')),
613
        (STATE_ERROR, _('Transfered')),
614
        (STATE_RETURNED, _('Returned')),
615
    ]
616

  
617
    resource = models.ForeignKey(
618
        Resource,
619
        verbose_name=_('Resource'))
620

  
621
    created = models.DateTimeField(
622
        verbose_name=_('Created'),
623
        auto_now_add=True)
624

  
625
    modified = models.DateTimeField(
626
        verbose_name=_('Created'),
627
        auto_now=True)
628

  
629
    filename = models.CharField(
630
        verbose_name=_('Identifier'),
631
        max_length=128)
632

  
633
    archive = models.FileField(
634
        verbose_name=_('Archive'),
635
        max_length=256)
636

  
637
    state = models.CharField(
638
        verbose_name=_('State'),
639
        choices=STATES,
640
        default=STATE_RECEIVED,
641
        max_length=16)
642

  
643
    url = models.URLField(
644
        verbose_name=_('URL'),
645
        blank=True)
646

  
647
    class Meta:
648
        verbose_name = _('MDEL request')
649
        verbose_name_plural = _('MDEL requests')
650
        unique_together = (
651
            ('resource', 'filename'),
652
        )
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

  
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 }}&nbsp;: {{ 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
<div id="requests" class="section">
40
    <h3>{% trans "Mappings" %} <a class="button" href="{% url "sp-fr-mapping-new" slug=object.slug %}">{% trans "Add" %}</a></h3>
41
    <table class="main">
42
        <thead>
43
            <tr>
44
                <td>Id</td>
45
                <td>Created</td>
46
                <td>Modified</td>
47
                <td>State</td>
48
                <td>Filename</td>
49
                <td>Slug</td>
50
                <td>Form Id</td>
51
            </tr>
52
        </thead>
53
        <tbody>
54
            {% for req in object.request_set.all %}
55
            <tr>
56
                <td>{{ req.id }}</td>
57
                <td>{{ req.created }}</td>
58
                <td>{{ req.modified }}</td>
59
                <td>{{ req.get_state_display }}</td>
60
                <td>{{ req.filename }}</td>
61
                <td>{{ req.content.formdef_slug }}</td>
62
                <td>{{ req.content.formdata_id }}</td>
63
            </tr>
64
            {% endfor %}
65

  
66
        </tbody>
67
    </table>
68
</div>
69
{% 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
188 188
.log-dialog table td {
189 189
        vertical-align: top;
190 190
}
191
.expression-widget input {
192
	width: 100%;
193
}
194
.variable-widget select {
195
	width: 100%;
196
}
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
-