Projet

Général

Profil

0001-general-remove-sp_fr-connector-68918.patch

Frédéric Péters, 10 septembre 2022 17:48

Télécharger (94,9 ko)

Voir les différences:

Subject: [PATCH] general: remove sp_fr connector (#68918)

 passerelle/apps/sp_fr/DOC.XSD                 | 108 ---
 passerelle/apps/sp_fr/__init__.py             |   0
 passerelle/apps/sp_fr/admin.py                |  34 -
 passerelle/apps/sp_fr/depotDossierPACS.XSD    | 129 ---
 passerelle/apps/sp_fr/fields.py               |  99 ---
 passerelle/apps/sp_fr/forms.py                |  66 --
 .../apps/sp_fr/migrations/0001_initial.py     | 141 ---
 .../migrations/0002_auto_20200504_1402.py     |  49 --
 .../sp_fr/migrations/0003_text_to_jsonb.py    |  18 -
 passerelle/apps/sp_fr/migrations/__init__.py  |   0
 passerelle/apps/sp_fr/models.py               | 805 ------------------
 passerelle/apps/sp_fr/recensementCitoyen.XSD  | 137 ---
 .../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      |  42 -
 passerelle/apps/sp_fr/urls.py                 |  32 -
 passerelle/apps/sp_fr/views.py                |  67 --
 passerelle/apps/sp_fr/xsd.py                  | 320 -------
 passerelle/settings.py                        |   1 -
 tests/wcs/test_sp_fr.py                       |  81 --
 21 files changed, 2199 deletions(-)
 delete mode 100644 passerelle/apps/sp_fr/DOC.XSD
 delete mode 100644 passerelle/apps/sp_fr/__init__.py
 delete mode 100644 passerelle/apps/sp_fr/admin.py
 delete mode 100644 passerelle/apps/sp_fr/depotDossierPACS.XSD
 delete mode 100644 passerelle/apps/sp_fr/fields.py
 delete mode 100644 passerelle/apps/sp_fr/forms.py
 delete mode 100644 passerelle/apps/sp_fr/migrations/0001_initial.py
 delete mode 100644 passerelle/apps/sp_fr/migrations/0002_auto_20200504_1402.py
 delete mode 100644 passerelle/apps/sp_fr/migrations/0003_text_to_jsonb.py
 delete mode 100644 passerelle/apps/sp_fr/migrations/__init__.py
 delete mode 100644 passerelle/apps/sp_fr/models.py
 delete mode 100644 passerelle/apps/sp_fr/recensementCitoyen.XSD
 delete mode 100644 passerelle/apps/sp_fr/templates/passerelle/widgets/variable_and_expression_widget.html
 delete mode 100644 passerelle/apps/sp_fr/templates/sp_fr/mapping_confirm_delete.html
 delete mode 100644 passerelle/apps/sp_fr/templates/sp_fr/mapping_form.html
 delete mode 100644 passerelle/apps/sp_fr/templates/sp_fr/resource_detail.html
 delete mode 100644 passerelle/apps/sp_fr/urls.py
 delete mode 100644 passerelle/apps/sp_fr/views.py
 delete mode 100644 passerelle/apps/sp_fr/xsd.py
 delete mode 100644 tests/wcs/test_sp_fr.py
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 }}&nbsp;: {{ 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
-