trivial: apply black (#49820)

This commit is contained in:
Frédéric Péters 2021-02-20 16:26:01 +01:00
parent 4540043a25
commit 3d9df1e526
412 changed files with 17549 additions and 11696 deletions

View File

@ -9,6 +9,6 @@ STATIC_ROOT = 'collected-static'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'passerelle.sqlite3', 'NAME': 'passerelle.sqlite3',
} }
} }

34
debian/settings.py vendored
View File

@ -12,12 +12,12 @@
# This file is sourced by "execfile" from /usr/lib/passerelle/debian_config.py # This file is sourced by "execfile" from /usr/lib/passerelle/debian_config.py
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
#DEBUG = False # DEBUG = False
#ADMINS = ( # ADMINS = (
# ('User 1', 'poulpe@example.org'), # ('User 1', 'poulpe@example.org'),
# ('User 2', 'janitor@example.net'), # ('User 2', 'janitor@example.net'),
#) # )
# ALLOWED_HOSTS must be correct in production! # ALLOWED_HOSTS must be correct in production!
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
@ -29,26 +29,26 @@ ALLOWED_HOSTS = ['*']
# Database # Database
# Warning: don't change ENGINE, it must be 'tenant_schemas.postgresql_backend' # Warning: don't change ENGINE, it must be 'tenant_schemas.postgresql_backend'
#DATABASES['default']['NAME'] = 'passerelle' # DATABASES['default']['NAME'] = 'passerelle'
#DATABASES['default']['USER'] = 'passerelle' # DATABASES['default']['USER'] = 'passerelle'
#DATABASES['default']['PASSWORD'] = '******' # DATABASES['default']['PASSWORD'] = '******'
#DATABASES['default']['HOST'] = 'localhost' # DATABASES['default']['HOST'] = 'localhost'
#DATABASES['default']['PORT'] = '5432' # DATABASES['default']['PORT'] = '5432'
LANGUAGE_CODE = 'fr-fr' LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris' TIME_ZONE = 'Europe/Paris'
# Email configuration # Email configuration
#EMAIL_SUBJECT_PREFIX = '[passerelle] ' # EMAIL_SUBJECT_PREFIX = '[passerelle] '
#SERVER_EMAIL = 'root@passerelle.example.org' # SERVER_EMAIL = 'root@passerelle.example.org'
#DEFAULT_FROM_EMAIL = 'webmaster@passerelle.example.org' # DEFAULT_FROM_EMAIL = 'webmaster@passerelle.example.org'
# SMTP configuration # SMTP configuration
#EMAIL_HOST = 'localhost' # EMAIL_HOST = 'localhost'
#EMAIL_HOST_USER = '' # EMAIL_HOST_USER = ''
#EMAIL_HOST_PASSWORD = '' # EMAIL_HOST_PASSWORD = ''
#EMAIL_PORT = 25 # EMAIL_PORT = 25
# HTTPS # HTTPS
#CSRF_COOKIE_SECURE = True # CSRF_COOKIE_SECURE = True
#SESSION_COOKIE_SECURE = True # SESSION_COOKIE_SECURE = True

View File

@ -9,15 +9,16 @@ def pytest_addoption(parser):
parser.addoption("--cmis-endpoint", help="Url of a passerelle CMIS endpoint") parser.addoption("--cmis-endpoint", help="Url of a passerelle CMIS endpoint")
parser.addoption("--cmis-username", help="Username for the CMIS endpoint") parser.addoption("--cmis-username", help="Username for the CMIS endpoint")
parser.addoption("--cmis-password", help="Password for the CMIS endpoint") parser.addoption("--cmis-password", help="Password for the CMIS endpoint")
parser.addoption( parser.addoption("--preserve-tree", action="store_true", default=False, help="Preserve test directory")
"--preserve-tree", action="store_true", default=False, help="Preserve test directory")
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def cmisclient(request): def cmisclient(request):
return cmislib.CmisClient( return cmislib.CmisClient(
request.config.getoption("--cmis-endpoint"), request.config.getoption("--cmis-username"), request.config.getoption("--cmis-endpoint"),
request.config.getoption("--cmis-password")) request.config.getoption("--cmis-username"),
request.config.getoption("--cmis-password"),
)
@pytest.fixture(scope='session') @pytest.fixture(scope='session')

View File

@ -10,11 +10,16 @@ import requests
SPECIAL_CHARS = '!#$%&+-^_`;[]{}+=' SPECIAL_CHARS = '!#$%&+-^_`;[]{}+='
@pytest.mark.parametrize("path,file_name", [ @pytest.mark.parametrize(
('', 'some.file'), ('/toto', 'some.file'), ('/toto/tata', 'some.file'), "path,file_name",
('/toto', 'some.other'), [
('/%s' % SPECIAL_CHARS, '%(spe)s.%(spe)s' % {'spe': SPECIAL_CHARS}) ('', 'some.file'),
]) ('/toto', 'some.file'),
('/toto/tata', 'some.file'),
('/toto', 'some.other'),
('/%s' % SPECIAL_CHARS, '%(spe)s.%(spe)s' % {'spe': SPECIAL_CHARS}),
],
)
def test_uploadfile(cmisclient, cmis_connector, cmis_tmpdir, tmpdir, monkeypatch, path, file_name): def test_uploadfile(cmisclient, cmis_connector, cmis_tmpdir, tmpdir, monkeypatch, path, file_name):
result_filename = 'result.file' result_filename = 'result.file'
monkeypatch.chdir(tmpdir) monkeypatch.chdir(tmpdir)
@ -25,9 +30,12 @@ def test_uploadfile(cmisclient, cmis_connector, cmis_tmpdir, tmpdir, monkeypatch
with orig_file.open('rb') as f: with orig_file.open('rb') as f:
file_b64_content = base64.b64encode(f.read()) file_b64_content = base64.b64encode(f.read())
response = requests.post( response = requests.post(
url, json={"path": cmis_tmpdir + path, url,
"file": {"content": file_b64_content, "filename": file_name, json={
"content_type": "image/jpeg"}}) "path": cmis_tmpdir + path,
"file": {"content": file_b64_content, "filename": file_name, "content_type": "image/jpeg"},
},
)
assert response.status_code == 200 assert response.status_code == 200
resp_data = response.json() resp_data = response.json()
assert resp_data['err'] == 0 assert resp_data['err'] == 0
@ -50,17 +58,23 @@ def test_uploadfile_conflict(cmisclient, cmis_connector, cmis_tmpdir, tmpdir, mo
url = urlparse.urljoin(cmis_connector, 'uploadfile') url = urlparse.urljoin(cmis_connector, 'uploadfile')
file_b64_content = base64.b64encode('file_content') file_b64_content = base64.b64encode('file_content')
response = requests.post( response = requests.post(
url, json={"path": cmis_tmpdir + '/uploadconflict', url,
"file": {"content": file_b64_content, "filename": 'some.file', json={
"content_type": "image/jpeg"}}) "path": cmis_tmpdir + '/uploadconflict',
"file": {"content": file_b64_content, "filename": 'some.file', "content_type": "image/jpeg"},
},
)
assert response.status_code == 200 assert response.status_code == 200
resp_data = response.json() resp_data = response.json()
assert resp_data['err'] == 0 assert resp_data['err'] == 0
file_b64_content = base64.b64encode('other_file_content') file_b64_content = base64.b64encode('other_file_content')
response = requests.post( response = requests.post(
url, json={"path": cmis_tmpdir + '/uploadconflict', url,
"file": {"content": file_b64_content, "filename": 'some.file', json={
"content_type": "image/jpeg"}}) "path": cmis_tmpdir + '/uploadconflict',
"file": {"content": file_b64_content, "filename": 'some.file', "content_type": "image/jpeg"},
},
)
assert response.status_code == 200 assert response.status_code == 200
resp_data = response.json() resp_data = response.json()
assert resp_data['err'] == 1 assert resp_data['err'] == 1

View File

@ -2,8 +2,7 @@ import pytest
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption( parser.addoption("--url", help="Url of a passerelle Planitech connector instance")
"--url", help="Url of a passerelle Planitech connector instance")
@pytest.fixture(scope='session') @pytest.fixture(scope='session')

View File

@ -7,10 +7,9 @@ import requests
def test_main(conn): def test_main(conn):
# get days # get days
query_string = parse.urlencode({ query_string = parse.urlencode(
'start_days': 1, 'end_days': 90, 'start_time': '10:00', 'end_time': '11:00', {'start_days': 1, 'end_days': 90, 'start_time': '10:00', 'end_time': '11:00', 'display': 'date'}
'display': 'date' )
})
url = conn + '/getfreegaps?%s' % query_string url = conn + '/getfreegaps?%s' % query_string
resp = requests.get(url) resp = requests.get(url)
resp.raise_for_status() resp.raise_for_status()
@ -20,10 +19,9 @@ def test_main(conn):
assert data assert data
# get places # get places
query_string = parse.urlencode({ query_string = parse.urlencode(
'start_days': 1, 'end_days': 90, 'start_time': '10:00', 'end_time': '11:00', {'start_days': 1, 'end_days': 90, 'start_time': '10:00', 'end_time': '11:00', 'display': 'place'}
'display': 'place' )
})
url = conn + '/getfreegaps?%s' % query_string url = conn + '/getfreegaps?%s' % query_string
resp = requests.get(url) resp = requests.get(url)
resp.raise_for_status() resp.raise_for_status()
@ -34,10 +32,16 @@ def test_main(conn):
place = data[random.randint(0, len(data) - 1)]['id'] place = data[random.randint(0, len(data) - 1)]['id']
# get days on one place # get days on one place
query_string = parse.urlencode({ query_string = parse.urlencode(
'start_days': 1, 'end_days': 90, 'start_time': '10:00', 'end_time': '11:00', {
'place_id': place, 'display': 'date' 'start_days': 1,
}) 'end_days': 90,
'start_time': '10:00',
'end_time': '11:00',
'place_id': place,
'display': 'date',
}
)
url = conn + '/getfreegaps?%s' % query_string url = conn + '/getfreegaps?%s' % query_string
resp = requests.get(url) resp = requests.get(url)
resp.raise_for_status() resp.raise_for_status()
@ -55,10 +59,19 @@ def test_main(conn):
chosen_date = data[0]['id'] chosen_date = data[0]['id']
# create reservation # create reservation
params = { params = {
'date': chosen_date, 'start_time': '10:00', 'end_time': '11:00', 'date': chosen_date,
'place_id': place, 'price': 200, 'name_id': 'john-doe', 'type_id': resa_type_id, 'start_time': '10:00',
'first_name': 'jon', 'last_name': 'doe', 'activity_id': activity_id, 'end_time': '11:00',
'email': 'jon.doe@localhost', 'object': 'reservation object', 'vat_rate': 200 'place_id': place,
'price': 200,
'name_id': 'john-doe',
'type_id': resa_type_id,
'first_name': 'jon',
'last_name': 'doe',
'activity_id': activity_id,
'email': 'jon.doe@localhost',
'object': 'reservation object',
'vat_rate': 200,
} }
print('Create reservation parameters \n') print('Create reservation parameters \n')
pprint.pprint(params) pprint.pprint(params)
@ -76,9 +89,7 @@ def test_main(conn):
reservation_id = data['reservation_id'] reservation_id = data['reservation_id']
# confirm reservation # confirm reservation
params = { params = {'reservation_id': reservation_id, 'status': 'standard'}
'reservation_id': reservation_id, 'status': 'standard'
}
url = conn + '/updatereservation' url = conn + '/updatereservation'
resp = requests.post(url, json=params) resp = requests.post(url, json=params)
resp.raise_for_status() resp.raise_for_status()

View File

@ -2,8 +2,7 @@ import pytest
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption( parser.addoption("--url", help="Url of a passerelle Toulouse Axel connector instance")
"--url", help="Url of a passerelle Toulouse Axel connector instance")
parser.addoption("--nameid", help="Publik Name ID") parser.addoption("--nameid", help="Publik Name ID")
parser.addoption("--firstname", help="first name of a user") parser.addoption("--firstname", help="first name of a user")
parser.addoption("--lastname", help="Last name of a user") parser.addoption("--lastname", help="Last name of a user")

View File

@ -45,19 +45,58 @@ def test_link(conn, user):
payload['DROITALIMAGE'] = 'NON' payload['DROITALIMAGE'] = 'NON'
payload['REVENUS']['CHOIXREVENU'] = '' payload['REVENUS']['CHOIXREVENU'] = ''
# remove non editable fields # remove non editable fields
for key in ['SITUATIONFAMILIALE', 'SITUATIONFAMILIALE_label', 'NBENFANTACTIF', 'NBRLACTIF', 'IDDUI', 'CODEMISEAJOUR', for key in [
'management_dates', 'annee_reference', 'annee_reference_label', 'annee_reference_short']: 'SITUATIONFAMILIALE',
'SITUATIONFAMILIALE_label',
'NBENFANTACTIF',
'NBRLACTIF',
'IDDUI',
'CODEMISEAJOUR',
'management_dates',
'annee_reference',
'annee_reference_label',
'annee_reference_short',
]:
payload.pop(key) payload.pop(key)
for key in ['IDPERSONNE', 'NOM', 'PRENOM', 'NOMJEUNEFILLE', 'DATENAISSANCE', 'CIVILITE', 'INDICATEURRL', 'CSP_label']: for key in [
'IDPERSONNE',
'NOM',
'PRENOM',
'NOMJEUNEFILLE',
'DATENAISSANCE',
'CIVILITE',
'INDICATEURRL',
'CSP_label',
]:
if 'RL1' in payload: if 'RL1' in payload:
payload['RL1'].pop(key) payload['RL1'].pop(key)
if 'RL2' in payload: if 'RL2' in payload:
payload['RL2'].pop(key) payload['RL2'].pop(key)
for key in ['MONTANTTOTAL', 'DATEVALIDITE', 'SFI', 'IREVENUS', 'RNF', 'NBENFANTSACHARGE', 'TYPEREGIME_label']: for key in [
'MONTANTTOTAL',
'DATEVALIDITE',
'SFI',
'IREVENUS',
'RNF',
'NBENFANTSACHARGE',
'TYPEREGIME_label',
]:
payload['REVENUS'].pop(key, None) payload['REVENUS'].pop(key, None)
for enfant in payload['ENFANT']: for enfant in payload['ENFANT']:
for key in ['id', 'text', 'NOM', 'DATENAISSANCE', 'SEXE', 'PRENOMPERE', 'PRENOMMERE', 'NOMPERE', 'NOMMERE', 'RATTACHEAUTREDUI', 'PRENOM', for key in [
'clae_cantine_current']: 'id',
'text',
'NOM',
'DATENAISSANCE',
'SEXE',
'PRENOMPERE',
'PRENOMMERE',
'NOMPERE',
'NOMMERE',
'RATTACHEAUTREDUI',
'PRENOM',
'clae_cantine_current',
]:
enfant.pop(key) enfant.pop(key)
enfant['AUTORISATIONURGENCEMEDICALE'] = 'OUI' enfant['AUTORISATIONURGENCEMEDICALE'] = 'OUI'
# manage contact fields # manage contact fields
@ -98,8 +137,10 @@ def test_link(conn, user):
# add partial update flags # add partial update flags
flags = [ flags = [
'maj:adresse', 'maj:adresse',
'maj:rl1', 'maj:rl1_adresse_employeur', 'maj:rl1',
'maj:rl2', 'maj:rl2_adresse_employeur', 'maj:rl1_adresse_employeur',
'maj:rl2',
'maj:rl2_adresse_employeur',
'maj:revenus', 'maj:revenus',
] ]
for i in range(0, 6): for i in range(0, 6):

View File

@ -2,8 +2,7 @@ import pytest
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption( parser.addoption("--url", help="Url of a passerelle Vivaticket connector instance")
"--url", help="Url of a passerelle Vivaticket connector instance")
@pytest.fixture(scope='session') @pytest.fixture(scope='session')

View File

@ -4,6 +4,7 @@ import datetime
import requests import requests
import random import random
def call_generic(conn, endpoint): def call_generic(conn, endpoint):
print("%s \n" % endpoint) print("%s \n" % endpoint)
url = conn + '/%s' % endpoint url = conn + '/%s' % endpoint
@ -21,17 +22,24 @@ def call_generic(conn, endpoint):
def test_get_events(conn): def test_get_events(conn):
call_generic(conn, 'events') call_generic(conn, 'events')
def test_get_rooms(conn): def test_get_rooms(conn):
call_generic(conn, 'rooms') call_generic(conn, 'rooms')
def test_get_themes(conn): def test_get_themes(conn):
call_generic(conn, 'themes') call_generic(conn, 'themes')
def test_book_event(conn): def test_book_event(conn):
url = conn + '/book' url = conn + '/book'
payload = {'id': 'formid', 'email': 'foo@example.com', payload = {
'datetime': datetime.datetime.now().strftime('%Y-%m-%dT%H:%M'), 'id': 'formid',
'room': '001', 'theme': 'A0001', 'quantity': 1 'email': 'foo@example.com',
'datetime': datetime.datetime.now().strftime('%Y-%m-%dT%H:%M'),
'room': '001',
'theme': 'A0001',
'quantity': 1,
} }
events = call_generic(conn, 'events') events = call_generic(conn, 'events')
random.shuffle(events) random.shuffle(events)
@ -42,7 +50,7 @@ def test_book_event(conn):
themes = call_generic(conn, 'themes') themes = call_generic(conn, 'themes')
random.shuffle(themes) random.shuffle(themes)
payload['theme'] = themes[0]['id'] payload['theme'] = themes[0]['id']
print("Creating booking with the following payload:\n%s" % payload) print("Creating booking with the following payload:\n%s" % payload)
resp = requests.post(url, json=payload) resp = requests.post(url, json=payload)
resp.raise_for_status() resp.raise_for_status()
res = resp.json() res = resp.json()

View File

@ -14,12 +14,38 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='ActesWeb', name='ActesWeb',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('log_level', models.CharField(default=b'INFO', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')])), (
('users', models.ManyToManyField(to='base.ApiUser', related_name='_actesweb_users_+', related_query_name='+', blank=True)), 'log_level',
models.CharField(
default=b'INFO',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
),
(
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_actesweb_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'verbose_name': "ActesWeb - Demande d'acte d'\xe9tat civil", 'verbose_name': "ActesWeb - Demande d'acte d'\xe9tat civil",

View File

@ -32,6 +32,8 @@ from passerelle.compat import json_loads
from passerelle.utils.api import endpoint from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError from passerelle.utils.jsonresponse import APIError
from passerelle.utils.conversion import ensure_encoding from passerelle.utils.conversion import ensure_encoding
@contextlib.contextmanager @contextlib.contextmanager
def named_tempfile(*args, **kwargs): def named_tempfile(*args, **kwargs):
with tempfile.NamedTemporaryFile(*args, **kwargs) as fp: with tempfile.NamedTemporaryFile(*args, **kwargs) as fp:
@ -46,8 +48,7 @@ class ActesWeb(BaseResource):
@property @property
def basepath(self): def basepath(self):
return os.path.join( return os.path.join(default_storage.path('actesweb'), self.slug)
default_storage.path('actesweb'), self.slug)
@endpoint(perm='can_access', methods=['post'], description=_('Create demand')) @endpoint(perm='can_access', methods=['post'], description=_('Create demand'))
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -88,6 +89,6 @@ class ActesWeb(BaseResource):
tempfile_name = tpf.name tempfile_name = tpf.name
os.rename(tempfile_name, filepath) os.rename(tempfile_name, filepath)
# set read only permission for owner and group # set read only permission for owner and group
os.chmod(filepath, stat.S_IRUSR|stat.S_IRGRP|stat.S_IWGRP) os.chmod(filepath, stat.S_IRUSR | stat.S_IRGRP | stat.S_IWGRP)
demand_id = '%s_%s' % (application_id, os.path.basename(filepath)) demand_id = '%s_%s' % (application_id, os.path.basename(filepath))
return {'data': {'demand_id': demand_id}} return {'data': {'demand_id': demand_id}}

View File

@ -14,12 +14,38 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='AirQuality', name='AirQuality',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('log_level', models.CharField(default=b'INFO', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')])), (
('users', models.ManyToManyField(to='base.ApiUser', related_name='_airquality_users_+', related_query_name='+', blank=True)), 'log_level',
models.CharField(
default=b'INFO',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
),
(
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_airquality_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'verbose_name': 'Air Quality', 'verbose_name': 'Air Quality',

View File

@ -28,24 +28,28 @@ from passerelle.utils.jsonresponse import APIError
class AirQuality(BaseResource): class AirQuality(BaseResource):
category = _('Misc') category = _('Misc')
api_description = _(u''' api_description = _(
u'''
This API provides a unique format for the air quality data of various places. This API provides a unique format for the air quality data of various places.
(But only supports the Rhône-Alpes region for now). (But only supports the Rhône-Alpes region for now).
''') '''
)
atmo_aura_api_token = models.CharField(max_length=100, atmo_aura_api_token = models.CharField(
verbose_name=_('ATMO AURA API token'), max_length=100, verbose_name=_('ATMO AURA API token'), blank=True, null=True
blank=True, null=True) )
class Meta: class Meta:
verbose_name = _('Air Quality') verbose_name = _('Air Quality')
@endpoint(pattern='^(?P<country>\w+)/(?P<city>\w+)/$', @endpoint(
example_pattern='{country}/{city}/', pattern='^(?P<country>\w+)/(?P<city>\w+)/$',
parameters={ example_pattern='{country}/{city}/',
'country': {'description': _('Country Code'), 'example_value': 'fr'}, parameters={
'city': {'description': _('City Name'), 'example_value': 'lyon'}, 'country': {'description': _('Country Code'), 'example_value': 'fr'},
}) 'city': {'description': _('City Name'), 'example_value': 'lyon'},
},
)
def details(self, request, country, city, **kwargs): def details(self, request, country, city, **kwargs):
methods = { methods = {
('fr', 'albertville'): 'air_rhonealpes', ('fr', 'albertville'): 'air_rhonealpes',
@ -82,7 +86,8 @@ class AirQuality(BaseResource):
'vienne': '38544', 'vienne': '38544',
} }
insee_code = insee_codes.get(city.lower()) insee_code = insee_codes.get(city.lower())
response = self.requests.get('https://api.atmo-aura.fr/communes/%s/indices' % insee_code, response = self.requests.get(
'https://api.atmo-aura.fr/communes/%s/indices' % insee_code,
params={'api_token': self.atmo_aura_api_token}, params={'api_token': self.atmo_aura_api_token},
) )
json_response = response.json() json_response = response.json()
@ -106,12 +111,13 @@ class AirQuality(BaseResource):
break break
if 'latest' in response_data: if 'latest' in response_data:
comment_response = self.requests.get('https://api.atmo-aura.fr/commentaire', comment_response = self.requests.get(
params={ 'https://api.atmo-aura.fr/commentaire',
'date': response_data['latest']['date'], params={
'api_token': self.atmo_aura_api_token, 'date': response_data['latest']['date'],
} 'api_token': self.atmo_aura_api_token,
) },
)
if comment_response.ok: if comment_response.ok:
response_data['comment'] = comment_response.json().get('commentaire') response_data['comment'] = comment_response.json().get('commentaire')

View File

@ -17,13 +17,29 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='APIEntreprise', name='APIEntreprise',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('slug', models.SlugField(unique=True, verbose_name='Identifier')), ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
('url', models.URLField(default=b'https://entreprise.api.gouv.fr/v2/', max_length=256, verbose_name='API URL')), (
'url',
models.URLField(
default=b'https://entreprise.api.gouv.fr/v2/', max_length=256, verbose_name='API URL'
),
),
('token', models.CharField(max_length=1024, verbose_name='API token')), ('token', models.CharField(max_length=1024, verbose_name='API token')),
('users', models.ManyToManyField(blank=True, related_name='_apientreprise_users_+', related_query_name='+', to='base.ApiUser')), (
'users',
models.ManyToManyField(
blank=True,
related_name='_apientreprise_users_+',
related_query_name='+',
to='base.ApiUser',
),
),
], ],
options={ options={
'verbose_name': 'API Entreprise', 'verbose_name': 'API Entreprise',

View File

@ -15,8 +15,8 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='apientreprise', model_name='apientreprise',
name='recipient', name='recipient',
field=models.CharField(default='', max_length=1024, verbose_name='Recipient', field=models.CharField(
help_text='default value' default='', max_length=1024, verbose_name='Recipient', help_text='default value'
), ),
preserve_default=False, preserve_default=False,
), ),

View File

@ -69,7 +69,7 @@ def normalize_results(data):
if tstamp > 0: if tstamp > 0:
try: try:
aware_date = make_aware(datetime.fromtimestamp(int(data[key]))) aware_date = make_aware(datetime.fromtimestamp(int(data[key])))
timestamp_to_datetime[key[:-len('timestamp')] + 'datetime'] = aware_date timestamp_to_datetime[key[: -len('timestamp')] + 'datetime'] = aware_date
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
# add converted timestamps to initial data # add converted timestamps to initial data
@ -81,8 +81,9 @@ class APIEntreprise(BaseResource):
url = models.URLField(_('API URL'), max_length=256, default='https://entreprise.api.gouv.fr/v2/') url = models.URLField(_('API URL'), max_length=256, default='https://entreprise.api.gouv.fr/v2/')
token = models.CharField(max_length=1024, verbose_name=_('API token')) token = models.CharField(max_length=1024, verbose_name=_('API token'))
recipient = models.CharField(max_length=1024, verbose_name=_('Recipient'), blank=False, recipient = models.CharField(
help_text=_('default value')) max_length=1024, verbose_name=_('Recipient'), blank=False, help_text=_('default value')
)
category = _('Business Process Connectors') category = _('Business Process Connectors')
@ -102,20 +103,20 @@ class APIEntreprise(BaseResource):
try: try:
response = self.requests.get(url, data=params, cache_duration=300) response = self.requests.get(url, data=params, cache_duration=300)
except requests.RequestException as e: except requests.RequestException as e:
raise APIError(u'API-entreprise connection error: %s' % raise APIError(u'API-entreprise connection error: %s' % exception_to_text(e), data=[])
exception_to_text(e), data=[])
try: try:
data = response.json() data = response.json()
except ValueError as e: except ValueError as e:
content = response.text[:1000] content = response.text[:1000]
raise APIError( raise APIError(
u'API-entreprise returned non-JSON content with status %s: %s' % u'API-entreprise returned non-JSON content with status %s: %s'
(response.status_code, content), % (response.status_code, content),
data={ data={
'status_code': response.status_code, 'status_code': response.status_code,
'exception': exception_to_text(e), 'exception': exception_to_text(e),
'content': content, 'content': content,
}) },
)
if response.status_code != 200: if response.status_code != 200:
if data.get('error') == 'not_found': if data.get('error') == 'not_found':
return { return {
@ -123,12 +124,12 @@ class APIEntreprise(BaseResource):
'err_desc': data.get('message', 'not-found'), 'err_desc': data.get('message', 'not-found'),
} }
raise APIError( raise APIError(
u'API-entreprise returned a non 200 status %s: %s' % u'API-entreprise returned a non 200 status %s: %s' % (response.status_code, data),
(response.status_code, data),
data={ data={
'status_code': response.status_code, 'status_code': response.status_code,
'content': data, 'content': data,
}) },
)
normalize_results(data) normalize_results(data)
return { return {
'err': 0, 'err': 0,
@ -138,13 +139,10 @@ class APIEntreprise(BaseResource):
# description of common endpoint parameters # description of common endpoint parameters
ASSOCIATION_PARAM = { ASSOCIATION_PARAM = {
'description': _('association SIREN or RNA/WALDEC number'), 'description': _('association SIREN or RNA/WALDEC number'),
'example_value': '44317013900036' 'example_value': '44317013900036',
} }
CONTEXT_PARAM = { CONTEXT_PARAM = {'description': _('request context: MPS, APS...'), 'example_value': 'APS'}
'description': _('request context: MPS, APS...'),
'example_value': 'APS'
}
MONTH_PARAM = { MONTH_PARAM = {
'description': _('requested month'), 'description': _('requested month'),
@ -153,12 +151,12 @@ class APIEntreprise(BaseResource):
OBJECT_PARAM = { OBJECT_PARAM = {
'description': _('request object: form number, file identifier...'), 'description': _('request object: form number, file identifier...'),
'example_value': '42' 'example_value': '42',
} }
RECIPIENT_PARAM = { RECIPIENT_PARAM = {
'description': _('request recipient: usually customer number'), 'description': _('request recipient: usually customer number'),
'example_value': '44317013900036' 'example_value': '44317013900036',
} }
SIREN_PARAM = { SIREN_PARAM = {
@ -166,26 +164,25 @@ class APIEntreprise(BaseResource):
'example_value': '443170139', 'example_value': '443170139',
} }
SIRET_PARAM = { SIRET_PARAM = {'description': _('firms SIRET number'), 'example_value': '44317013900036'}
'description': _('firms SIRET number'),
'example_value': '44317013900036'
}
YEAR_PARAM = { YEAR_PARAM = {
'description': _('requested year'), 'description': _('requested year'),
'example_value': '2019', 'example_value': '2019',
} }
@endpoint(perm='can_access', @endpoint(
pattern=r'(?P<association_id>\w+)/$', perm='can_access',
example_pattern='{association_id}/', pattern=r'(?P<association_id>\w+)/$',
description=_('Get association\'s documents'), example_pattern='{association_id}/',
parameters={ description=_('Get association\'s documents'),
'association_id': ASSOCIATION_PARAM, parameters={
'object': OBJECT_PARAM, 'association_id': ASSOCIATION_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def documents_associations(self, request, association_id, **kwargs): def documents_associations(self, request, association_id, **kwargs):
data = [] data = []
resp = self.get('documents_associations/%s/' % association_id, **kwargs) resp = self.get('documents_associations/%s/' % association_id, **kwargs)
@ -193,19 +190,24 @@ class APIEntreprise(BaseResource):
# ignore documents with no type # ignore documents with no type
if not item.get('type'): if not item.get('type'):
continue continue
signature_elements = {'url': item['url'], signature_elements = {
'context': kwargs['context'], 'url': item['url'],
'object': kwargs['object'], 'context': kwargs['context'],
'recipient': kwargs['recipient']} 'object': kwargs['object'],
'recipient': kwargs['recipient'],
}
signature = signing.dumps(signature_elements) signature = signing.dumps(signature_elements)
document_url = request.build_absolute_uri( document_url = request.build_absolute_uri(
reverse('generic-endpoint', reverse(
kwargs={ 'generic-endpoint',
'connector': self.get_connector_slug(), kwargs={
'slug': self.slug, 'connector': self.get_connector_slug(),
'endpoint': 'document', 'slug': self.slug,
'rest': '%s/%s/' % (association_id, signature), 'endpoint': 'document',
})) 'rest': '%s/%s/' % (association_id, signature),
},
)
)
item['id'] = item['timestamp'] item['id'] = item['timestamp']
item['text'] = item['type'] item['text'] = item['type']
item['url'] = document_url item['url'] = document_url
@ -214,19 +216,21 @@ class APIEntreprise(BaseResource):
data.sort(key=lambda i: i['id']) data.sort(key=lambda i: i['id'])
return {'err': 0, 'data': data} return {'err': 0, 'data': data}
@endpoint(pattern=r'(?P<association_id>\w+)/(?P<document_id>[\:\w-]+)/$', @endpoint(
example_pattern='{association_id}/{document_id}/', pattern=r'(?P<association_id>\w+)/(?P<document_id>[\:\w-]+)/$',
description=_('Get association\'s document'), example_pattern='{association_id}/{document_id}/',
parameters={ description=_('Get association\'s document'),
'association_id': ASSOCIATION_PARAM, parameters={
'document_id': { 'association_id': ASSOCIATION_PARAM,
'description': _('document id'), 'document_id': {
'example_value': 'A1500660325', 'description': _('document id'),
}, 'example_value': 'A1500660325',
'object': OBJECT_PARAM, },
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def document(self, request, association_id, document_id, **kwargs): def document(self, request, association_id, document_id, **kwargs):
try: try:
params = signing.loads(document_id, max_age=DOCUMENT_SIGNATURE_MAX_AGE) params = signing.loads(document_id, max_age=DOCUMENT_SIGNATURE_MAX_AGE)
@ -237,20 +241,22 @@ class APIEntreprise(BaseResource):
return HttpResponse(response, content_type='application/pdf') return HttpResponse(response, content_type='application/pdf')
raise Http404('document not found') raise Http404('document not found')
@endpoint(name='document_association', @endpoint(
pattern=r'(?P<association_id>\w+)/get-last/$', name='document_association',
example_pattern='{association_id}/get-last/', pattern=r'(?P<association_id>\w+)/get-last/$',
description=_('Get association\'s last document of type'), example_pattern='{association_id}/get-last/',
parameters={ description=_('Get association\'s last document of type'),
'association_id': ASSOCIATION_PARAM, parameters={
'document_type': { 'association_id': ASSOCIATION_PARAM,
'description': _('document type'), 'document_type': {
'example_value': 'Statuts', 'description': _('document type'),
}, 'example_value': 'Statuts',
'object': OBJECT_PARAM, },
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def get_last_document_of_type(self, request, association_id, document_type, **kwargs): def get_last_document_of_type(self, request, association_id, document_type, **kwargs):
document = None document = None
resp = self.get('documents_associations/%s/' % association_id, **kwargs) resp = self.get('documents_associations/%s/' % association_id, **kwargs)
@ -260,46 +266,49 @@ class APIEntreprise(BaseResource):
document = documents[-1] document = documents[-1]
return {'data': document} return {'data': document}
@endpoint(perm='can_access', @endpoint(
pattern=r'(?P<siren>\w+)/$', perm='can_access',
example_pattern='{siren}/', pattern=r'(?P<siren>\w+)/$',
description=_('Get firm\'s data from Infogreffe'), example_pattern='{siren}/',
parameters={ description=_('Get firm\'s data from Infogreffe'),
'siren': SIREN_PARAM, parameters={
'object': OBJECT_PARAM, 'siren': SIREN_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def extraits_rcs(self, request, siren, **kwargs): def extraits_rcs(self, request, siren, **kwargs):
return self.get('extraits_rcs_infogreffe/%s/' % siren, **kwargs) return self.get('extraits_rcs_infogreffe/%s/' % siren, **kwargs)
@endpoint(perm='can_access', @endpoint(
pattern=r'(?P<association_id>\w+)/$', perm='can_access',
example_pattern='{association_id}/', pattern=r'(?P<association_id>\w+)/$',
description=_('Get association\'s related informations'), example_pattern='{association_id}/',
parameters={ description=_('Get association\'s related informations'),
'association_id': ASSOCIATION_PARAM, parameters={
'object': OBJECT_PARAM, 'association_id': ASSOCIATION_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def associations(self, request, association_id, **kwargs): def associations(self, request, association_id, **kwargs):
return self.get('associations/%s/' % association_id, **kwargs) return self.get('associations/%s/' % association_id, **kwargs)
@endpoint(perm='can_access', @endpoint(
pattern=r'(?P<siren>\w+)/$', perm='can_access',
example_pattern='{siren}/', pattern=r'(?P<siren>\w+)/$',
description=_('Get firm\'s related informations'), example_pattern='{siren}/',
parameters={ description=_('Get firm\'s related informations'),
'siren': SIREN_PARAM, parameters={
'object': OBJECT_PARAM, 'siren': SIREN_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
'include_private': { 'recipient': RECIPIENT_PARAM,
'description': _('Include private informations'), 'include_private': {'description': _('Include private informations'), 'example_value': 'true'},
'example_value': 'true' },
} )
})
def entreprises(self, request, siren, include_private=False, **kwargs): def entreprises(self, request, siren, include_private=False, **kwargs):
if len(siren) != 9: if len(siren) != 9:
raise APIError(_('invalid SIREN length (must be 9 characters)')) raise APIError(_('invalid SIREN length (must be 9 characters)'))
@ -307,60 +316,68 @@ class APIEntreprise(BaseResource):
kwargs['non_diffusables'] = True kwargs['non_diffusables'] = True
return self.get('entreprises/%s/' % siren, **kwargs) return self.get('entreprises/%s/' % siren, **kwargs)
@endpoint(perm='can_access', @endpoint(
methods=['get'], perm='can_access',
pattern=r'(?P<siret>\w+)/$', methods=['get'],
example_pattern='{siret}/', pattern=r'(?P<siret>\w+)/$',
description_get=_('Get firms\'s related informations'), example_pattern='{siret}/',
parameters={ description_get=_('Get firms\'s related informations'),
'siret': SIRET_PARAM, parameters={
'object': OBJECT_PARAM, 'siret': SIRET_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def etablissements(self, request, siret, **kwargs): def etablissements(self, request, siret, **kwargs):
return self.get('etablissements/%s/' % siret, **kwargs) return self.get('etablissements/%s/' % siret, **kwargs)
@endpoint(perm='can_access', @endpoint(
methods=['get'], perm='can_access',
pattern=r'(?P<siret>\w+)/$', methods=['get'],
example_pattern='{siret}/', pattern=r'(?P<siret>\w+)/$',
description_get=_('Get firms\'s financial year informations'), example_pattern='{siret}/',
parameters={ description_get=_('Get firms\'s financial year informations'),
'siret': SIRET_PARAM, parameters={
'object': OBJECT_PARAM, 'siret': SIRET_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def exercices(self, request, siret, **kwargs): def exercices(self, request, siret, **kwargs):
return self.get('exercices/%s/' % siret, **kwargs) return self.get('exercices/%s/' % siret, **kwargs)
@endpoint(perm='can_access', @endpoint(
pattern=r'(?P<siren>\w+)/$', perm='can_access',
example_pattern='{siren}/', pattern=r'(?P<siren>\w+)/$',
description=_('Get firm\'s annual workforce data'), example_pattern='{siren}/',
parameters={ description=_('Get firm\'s annual workforce data'),
'siren': SIREN_PARAM, parameters={
'object': OBJECT_PARAM, 'siren': SIREN_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def effectifs_annuels_acoss_covid(self, request, siren, **kwargs): def effectifs_annuels_acoss_covid(self, request, siren, **kwargs):
if len(siren) != 9: if len(siren) != 9:
raise APIError(_('invalid SIREN length (must be 9 characters)')) raise APIError(_('invalid SIREN length (must be 9 characters)'))
return self.get('effectifs_annuels_acoss_covid/%s/' % siren, **kwargs) return self.get('effectifs_annuels_acoss_covid/%s/' % siren, **kwargs)
@endpoint(perm='can_access', @endpoint(
pattern=r'(?P<year>\w+)/(?P<month>\w+)/(?P<siren>\w+)/$', perm='can_access',
description=_('Get firm\'s monthly workforce data, by SIREN'), pattern=r'(?P<year>\w+)/(?P<month>\w+)/(?P<siren>\w+)/$',
parameters={ description=_('Get firm\'s monthly workforce data, by SIREN'),
'year': YEAR_PARAM, parameters={
'month': MONTH_PARAM, 'year': YEAR_PARAM,
'siren': SIREN_PARAM, 'month': MONTH_PARAM,
'object': OBJECT_PARAM, 'siren': SIREN_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def entreprise_effectifs_mensuels_acoss_covid(self, request, year, month, siren, **kwargs): def entreprise_effectifs_mensuels_acoss_covid(self, request, year, month, siren, **kwargs):
if len(siren) != 9: if len(siren) != 9:
raise APIError(_('invalid SIREN length (must be 9 characters)')) raise APIError(_('invalid SIREN length (must be 9 characters)'))
@ -369,17 +386,19 @@ class APIEntreprise(BaseResource):
'effectifs_mensuels_acoss_covid/%s/%s/entreprise/%s/' % (year, month, siren), **kwargs 'effectifs_mensuels_acoss_covid/%s/%s/entreprise/%s/' % (year, month, siren), **kwargs
) )
@endpoint(perm='can_access', @endpoint(
pattern=r'(?P<year>\w+)/(?P<month>\w+)/(?P<siret>\w+)/$', perm='can_access',
description=_('Get firm\'s monthly workforce data, by SIRET'), pattern=r'(?P<year>\w+)/(?P<month>\w+)/(?P<siret>\w+)/$',
parameters={ description=_('Get firm\'s monthly workforce data, by SIRET'),
'year': YEAR_PARAM, parameters={
'month': MONTH_PARAM, 'year': YEAR_PARAM,
'siret': SIRET_PARAM, 'month': MONTH_PARAM,
'object': OBJECT_PARAM, 'siret': SIRET_PARAM,
'context': CONTEXT_PARAM, 'object': OBJECT_PARAM,
'recipient': RECIPIENT_PARAM, 'context': CONTEXT_PARAM,
}) 'recipient': RECIPIENT_PARAM,
},
)
def etablissement_effectifs_mensuels_acoss_covid(self, request, year, month, siret, **kwargs): def etablissement_effectifs_mensuels_acoss_covid(self, request, year, month, siret, **kwargs):
month = month.zfill(2) month = month.zfill(2)
return self.get( return self.get(

View File

@ -29,7 +29,7 @@ KNOWN_ERRORS = {
'Il existe au moins un enfant pour lequel il existe un droit sur le dossier et/ou à la période demandée', 'Il existe au moins un enfant pour lequel il existe un droit sur le dossier et/ou à la période demandée',
'Il existe des droits pour la prestation sélectionnée sur le dossier et/ou la période demandée', 'Il existe des droits pour la prestation sélectionnée sur le dossier et/ou la période demandée',
'Il existe des droits pour la prestation sélectionnée sur le dossier et/ou la période demandée (après date du jour)', 'Il existe des droits pour la prestation sélectionnée sur le dossier et/ou la période demandée (après date du jour)',
'Lopérateurs téléphonique» ne propose pas de raccordement SMS avec un prestataire externe (raccordement avec un numéro court). ' 'Lopérateurs téléphonique» ne propose pas de raccordement SMS avec un prestataire externe (raccordement avec un numéro court). ',
}, },
500: { 500: {
'Les informations souhaitées sont momentanément indisponibles. Merci de renouveler votre demande ultérieurement.', 'Les informations souhaitées sont momentanément indisponibles. Merci de renouveler votre demande ultérieurement.',
@ -42,6 +42,6 @@ KNOWN_ERRORS = {
"Votre demande n'a pu aboutir en raison d'un incident technique momentané. Merci de renouveler votre demande ultérieurement.", "Votre demande n'a pu aboutir en raison d'un incident technique momentané. Merci de renouveler votre demande ultérieurement.",
"Votre demande n'a pu aboutir en raison d'une erreur fonctionnelle lié à l'appel au service IMC.", "Votre demande n'a pu aboutir en raison d'une erreur fonctionnelle lié à l'appel au service IMC.",
"Votre demande n'a pu aboutir en raison d'une erreur technique lié à l'appel au service IMC.", "Votre demande n'a pu aboutir en raison d'une erreur technique lié à l'appel au service IMC.",
"Votre demande na pu aboutir en raison d'un problème technique lié aux données entrantes du webservice. Merci de renouveler votre demande ultérieurement." "Votre demande na pu aboutir en raison d'un problème technique lié aux données entrantes du webservice. Merci de renouveler votre demande ultérieurement.",
} },
} }

View File

@ -14,14 +14,50 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='APIParticulier', name='APIParticulier',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('log_level', models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')])), (
('_platform', models.CharField(choices=[(b'prod', 'Production'), (b'test', 'Test')], max_length=8, verbose_name='Platform')), 'log_level',
('_api_key', models.CharField(default=b'', max_length=64, verbose_name='API key', blank=True)), models.CharField(
('users', models.ManyToManyField(to='base.ApiUser', related_name='_apiparticulier_users_+', related_query_name='+', blank=True)), default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
),
(
'_platform',
models.CharField(
choices=[(b'prod', 'Production'), (b'test', 'Test')],
max_length=8,
verbose_name='Platform',
),
),
(
'_api_key',
models.CharField(default=b'', max_length=64, verbose_name='API key', blank=True),
),
(
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_apiparticulier_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'abstract': False, 'abstract': False,

View File

@ -42,11 +42,7 @@ from .known_errors import KNOWN_ERRORS
class APIParticulier(BaseResource): class APIParticulier(BaseResource):
PLATFORMS = [ PLATFORMS = [
{ {'name': 'prod', 'label': _('Production'), 'url': 'https://particulier.api.gouv.fr/api/'},
'name': 'prod',
'label': _('Production'),
'url': 'https://particulier.api.gouv.fr/api/'
},
{ {
'name': 'test', 'name': 'test',
'label': _('Test'), 'label': _('Test'),
@ -58,13 +54,10 @@ class APIParticulier(BaseResource):
platform = models.CharField( platform = models.CharField(
verbose_name=_('Platform'), verbose_name=_('Platform'),
max_length=8, max_length=8,
choices=[(key, platform['label']) for key, platform in PLATFORMS.items()]) choices=[(key, platform['label']) for key, platform in PLATFORMS.items()],
)
api_key = models.CharField( api_key = models.CharField(max_length=256, default='', blank=True, verbose_name=_('API key'))
max_length=256,
default='',
blank=True,
verbose_name=_('API key'))
log_requests_errors = False log_requests_errors = False
@ -79,28 +72,24 @@ class APIParticulier(BaseResource):
if user: if user:
headers['X-User'] = user headers['X-User'] = user
try: try:
response = self.requests.get( response = self.requests.get(url, headers=headers, timeout=5, **kwargs)
url,
headers=headers,
timeout=5,
**kwargs)
except requests.RequestException as e: except requests.RequestException as e:
raise APIError( raise APIError(
u'API-particulier platform "%s" connection error: %s' % u'API-particulier platform "%s" connection error: %s' % (self.platform, exception_to_text(e)),
(self.platform, exception_to_text(e)),
log_error=True, log_error=True,
data={ data={
'code': 'connection-error', 'code': 'connection-error',
'platform': self.platform, 'platform': self.platform,
'error': six.text_type(e), 'error': six.text_type(e),
}) },
)
try: try:
data = response.json() data = response.json()
except JSONDecodeError as e: except JSONDecodeError as e:
content = repr(response.content[:1000]) content = repr(response.content[:1000])
raise APIError( raise APIError(
u'API-particulier platform "%s" returned non-JSON content with status %s: %s' % u'API-particulier platform "%s" returned non-JSON content with status %s: %s'
(self.platform, response.status_code, content), % (self.platform, response.status_code, content),
log_error=True, log_error=True,
data={ data={
'code': 'non-json', 'code': 'non-json',
@ -108,7 +97,8 @@ class APIParticulier(BaseResource):
'exception': six.text_type(e), 'exception': six.text_type(e),
'platform': self.platform, 'platform': self.platform,
'content': content, 'content': content,
}) },
)
if response.status_code != 200: if response.status_code != 200:
# avoid logging http errors about non-transport failure # avoid logging http errors about non-transport failure
message = data.get('message', '') message = data.get('message', '')
@ -120,162 +110,180 @@ class APIParticulier(BaseResource):
'status_code': response.status_code, 'status_code': response.status_code,
'platform': self.platform, 'platform': self.platform,
'content': data, 'content': data,
}) },
)
raise APIError( raise APIError(
u'API-particulier platform "%s" returned a non 200 status %s: %s' % u'API-particulier platform "%s" returned a non 200 status %s: %s'
(self.platform, response.status_code, data), % (self.platform, response.status_code, data),
log_error=True, log_error=True,
data={ data={
'code': 'non-200', 'code': 'non-200',
'status_code': response.status_code, 'status_code': response.status_code,
'platform': self.platform, 'platform': self.platform,
'content': data, 'content': data,
}) },
)
return { return {
'err': 0, 'err': 0,
'data': data, 'data': data,
} }
@endpoint(perm='can_access', @endpoint(
show=False, perm='can_access',
description=_('Get citizen\'s fiscal informations'), show=False,
parameters={ description=_('Get citizen\'s fiscal informations'),
'numero_fiscal': { parameters={
'description': _('fiscal identifier'), 'numero_fiscal': {
'example_value': '1562456789521', 'description': _('fiscal identifier'),
}, 'example_value': '1562456789521',
'reference_avis': { },
'description': _('tax notice number'), 'reference_avis': {
'example_value': '1512456789521', 'description': _('tax notice number'),
}, 'example_value': '1512456789521',
'user': { },
'description': _('requesting user'), 'user': {
'example_value': 'John Doe (agent)', 'description': _('requesting user'),
}, 'example_value': 'John Doe (agent)',
}) },
},
)
def impots_svair(self, request, numero_fiscal, reference_avis, user=None): def impots_svair(self, request, numero_fiscal, reference_avis, user=None):
# deprecated endpoint # deprecated endpoint
return self.v2_avis_imposition(request, numero_fiscal, reference_avis, user=user) return self.v2_avis_imposition(request, numero_fiscal, reference_avis, user=user)
@endpoint(name='avis-imposition', @endpoint(
perm='can_access', name='avis-imposition',
description=_('Get citizen\'s fiscal informations'), perm='can_access',
parameters={ description=_('Get citizen\'s fiscal informations'),
'numero_fiscal': { parameters={
'description': _('fiscal identifier'), 'numero_fiscal': {
'example_value': '1562456789521', 'description': _('fiscal identifier'),
}, 'example_value': '1562456789521',
'reference_avis': { },
'description': _('tax notice number'), 'reference_avis': {
'example_value': '1512456789521', 'description': _('tax notice number'),
}, 'example_value': '1512456789521',
'user': { },
'description': _('requesting user'), 'user': {
'example_value': 'John Doe (agent)', 'description': _('requesting user'),
}, 'example_value': 'John Doe (agent)',
}, },
json_schema_response={ },
'type': 'object', json_schema_response={
'required': ['err'], 'type': 'object',
'properties': { 'required': ['err'],
'err': {'enum': [0, 1]}, 'properties': {
'declarant1': { 'err': {'enum': [0, 1]},
'type': 'object', 'declarant1': {
'properties': { 'type': 'object',
'nom': {'type': 'string'}, 'properties': {
'nomNaissance': {'type': 'string'}, 'nom': {'type': 'string'},
'prenoms': {'type': 'string'}, 'nomNaissance': {'type': 'string'},
'dateNaissance': {'type': 'string'} 'prenoms': {'type': 'string'},
}, 'dateNaissance': {'type': 'string'},
}, },
'declarant2': { },
'type': 'object', 'declarant2': {
'properties': { 'type': 'object',
'nom': {'type': 'string'}, 'properties': {
'nomNaissance': {'type': 'string'}, 'nom': {'type': 'string'},
'prenoms': {'type': 'string'}, 'nomNaissance': {'type': 'string'},
'dateNaissance': {'type': 'string'} 'prenoms': {'type': 'string'},
} 'dateNaissance': {'type': 'string'},
}, },
'foyerFiscal': { },
'type': 'object', 'foyerFiscal': {
'properties': { 'type': 'object',
'annee': {'type': 'integer'}, 'properties': {
'adresse': {'type': 'string'}, 'annee': {'type': 'integer'},
} 'adresse': {'type': 'string'},
}, },
'dateRecouvrement': {'type': 'string', 'pattern': r'^\d{1,2}/\d{1,2}/\d{4}$'}, },
'dateEtablissement': {'type': 'string', 'pattern': r'^\d{1,2}/\d{1,2}/\d{4}$'}, 'dateRecouvrement': {'type': 'string', 'pattern': r'^\d{1,2}/\d{1,2}/\d{4}$'},
'nombreParts': {'type': 'integer'}, 'dateEtablissement': {'type': 'string', 'pattern': r'^\d{1,2}/\d{1,2}/\d{4}$'},
'situationFamille': {'type': 'string'}, 'nombreParts': {'type': 'integer'},
'nombrePersonnesCharge': {'type': 'integer'}, 'situationFamille': {'type': 'string'},
'revenuBrutGlobal': {'type': 'integer'}, 'nombrePersonnesCharge': {'type': 'integer'},
'revenuImposable': {'type': 'integer'}, 'revenuBrutGlobal': {'type': 'integer'},
'impotRevenuNetAvantCorrections': {'type': 'integer'}, 'revenuImposable': {'type': 'integer'},
'montantImpot': {'type': 'integer'}, 'impotRevenuNetAvantCorrections': {'type': 'integer'},
'revenuFiscalReference': {'type': 'integer'}, 'montantImpot': {'type': 'integer'},
'anneeImpots': {'type': 'string', 'pattern': r'^[0-9]{4}$'}, 'revenuFiscalReference': {'type': 'integer'},
'anneeRevenus': {'type': 'string', 'pattern': r'^[0-9]{4}$'}, 'anneeImpots': {'type': 'string', 'pattern': r'^[0-9]{4}$'},
'erreurCorrectif': {'type': 'string'}, 'anneeRevenus': {'type': 'string', 'pattern': r'^[0-9]{4}$'},
'situationPartielle': {'type': 'string'} 'erreurCorrectif': {'type': 'string'},
} 'situationPartielle': {'type': 'string'},
}) },
},
)
def v2_avis_imposition(self, request, numero_fiscal, reference_avis, user=None): def v2_avis_imposition(self, request, numero_fiscal, reference_avis, user=None):
numero_fiscal = numero_fiscal.strip()[:13] numero_fiscal = numero_fiscal.strip()[:13]
reference_avis = reference_avis.strip()[:13] reference_avis = reference_avis.strip()[:13]
if len(numero_fiscal) < 13 or len(reference_avis) < 13: if len(numero_fiscal) < 13 or len(reference_avis) < 13:
raise APIError('bad numero_fiscal or reference_avis, must be 13 chars long', status_code=400) raise APIError('bad numero_fiscal or reference_avis, must be 13 chars long', status_code=400)
return self.get('v2/avis-imposition', params={ return self.get(
'numeroFiscal': numero_fiscal, 'v2/avis-imposition',
'referenceAvis': reference_avis, params={
}, user=user) 'numeroFiscal': numero_fiscal,
'referenceAvis': reference_avis,
},
user=user,
)
@endpoint(perm='can_access', @endpoint(
show=False, perm='can_access',
description=_('Get family allowances recipient informations'), show=False,
parameters={ description=_('Get family allowances recipient informations'),
'code_postal': { parameters={
'description': _('postal code'), 'code_postal': {
'example_value': '99148', 'description': _('postal code'),
}, 'example_value': '99148',
'numero_allocataire': { },
'description': _('recipient identifier'), 'numero_allocataire': {
'example_value': '0000354', 'description': _('recipient identifier'),
}, 'example_value': '0000354',
'user': { },
'description': _('requesting user'), 'user': {
'example_value': 'John Doe (agent)', 'description': _('requesting user'),
}, 'example_value': 'John Doe (agent)',
}) },
},
)
def caf_famille(self, request, code_postal, numero_allocataire, user=None): def caf_famille(self, request, code_postal, numero_allocataire, user=None):
# deprecated endpoint # deprecated endpoint
return self.v2_situation_familiale(request, code_postal, numero_allocataire, user=user) return self.v2_situation_familiale(request, code_postal, numero_allocataire, user=user)
@endpoint(name='situation-familiale', @endpoint(
perm='can_access', name='situation-familiale',
description=_('Get family allowances recipient informations'), perm='can_access',
parameters={ description=_('Get family allowances recipient informations'),
'code_postal': { parameters={
'description': _('postal code'), 'code_postal': {
'example_value': '99148', 'description': _('postal code'),
}, 'example_value': '99148',
'numero_allocataire': { },
'description': _('recipient identifier'), 'numero_allocataire': {
'example_value': '0000354', 'description': _('recipient identifier'),
}, 'example_value': '0000354',
'user': { },
'description': _('requesting user'), 'user': {
'example_value': 'John Doe (agent)', 'description': _('requesting user'),
}, 'example_value': 'John Doe (agent)',
}) },
},
)
def v2_situation_familiale(self, request, code_postal, numero_allocataire, user=None): def v2_situation_familiale(self, request, code_postal, numero_allocataire, user=None):
if not code_postal.strip() or not numero_allocataire.strip(): if not code_postal.strip() or not numero_allocataire.strip():
raise APIError('missing code_postal or numero_allocataire', status_code=400) raise APIError('missing code_postal or numero_allocataire', status_code=400)
return self.get('v2/composition-familiale', params={ return self.get(
'codePostal': code_postal, 'v2/composition-familiale',
'numeroAllocataire': numero_allocataire, params={
}, user=user) 'codePostal': code_postal,
'numeroAllocataire': numero_allocataire,
},
user=user,
)
category = _('Business Process Connectors') category = _('Business Process Connectors')

View File

@ -14,13 +14,36 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Arcgis', name='Arcgis',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('log_level', models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')])), (
'log_level',
models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
),
('base_url', models.CharField(max_length=256, verbose_name='SIG Url')), ('base_url', models.CharField(max_length=256, verbose_name='SIG Url')),
('users', models.ManyToManyField(to='base.ApiUser', related_name='_arcgis_users_+', related_query_name='+', blank=True)), (
'users',
models.ManyToManyField(
to='base.ApiUser', related_name='_arcgis_users_+', related_query_name='+', blank=True
),
),
], ],
options={ options={
'verbose_name': 'Arcgis Webservice', 'verbose_name': 'Arcgis Webservice',

View File

@ -29,7 +29,9 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='arcgis', model_name='arcgis',
name='client_certificate', name='client_certificate',
field=models.FileField(blank=True, null=True, upload_to=b'', verbose_name='TLS client certificate'), field=models.FileField(
blank=True, null=True, upload_to=b'', verbose_name='TLS client certificate'
),
), ),
migrations.AddField( migrations.AddField(
model_name='arcgis', model_name='arcgis',
@ -54,6 +56,18 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='arcgis', model_name='arcgis',
name='log_level', name='log_level',
field=models.CharField(choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')], default=b'INFO', max_length=10, verbose_name='Log Level'), field=models.CharField(
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
default=b'INFO',
max_length=10,
verbose_name='Log Level',
),
), ),
] ]

View File

@ -18,17 +18,51 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Query', name='Query',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('slug', models.SlugField(max_length=128, verbose_name='Slug')), ('slug', models.SlugField(max_length=128, verbose_name='Slug')),
('description', models.TextField(blank=True, verbose_name='Description')), ('description', models.TextField(blank=True, verbose_name='Description')),
('folder', models.CharField(blank=True, max_length=64, verbose_name='ArcGis Folder')), ('folder', models.CharField(blank=True, max_length=64, verbose_name='ArcGis Folder')),
('service', models.CharField(max_length=64, verbose_name='ArcGis Service')), ('service', models.CharField(max_length=64, verbose_name='ArcGis Service')),
('layer', models.CharField(blank=True, max_length=8, verbose_name='ArcGis Layer')), ('layer', models.CharField(blank=True, max_length=8, verbose_name='ArcGis Layer')),
('where', models.TextField(blank=True, help_text="<span>Use syntax <tt>{name}</tt> to introduce a string parameter and <tt>{name:d}</tt> for a decimal parameter. ex.:<br/><tt>adress LIKE ('%' || UPPER({adress}) || '%')</tt><br/><tt>population < {population:d}</tt></span>", validators=[passerelle.apps.arcgis.models.validate_where], verbose_name='ArcGis Where Clause')), (
('id_template', models.TextField(blank=True, help_text="Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}", validators=[passerelle.utils.templates.validate_template], verbose_name='Id template')), 'where',
('text_template', models.TextField(blank=True, help_text="Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}", validators=[passerelle.utils.templates.validate_template], verbose_name='Text template')), models.TextField(
('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='arcgis.ArcGIS', verbose_name='Resource')), blank=True,
help_text="<span>Use syntax <tt>{name}</tt> to introduce a string parameter and <tt>{name:d}</tt> for a decimal parameter. ex.:<br/><tt>adress LIKE ('%' || UPPER({adress}) || '%')</tt><br/><tt>population < {population:d}</tt></span>",
validators=[passerelle.apps.arcgis.models.validate_where],
verbose_name='ArcGis Where Clause',
),
),
(
'id_template',
models.TextField(
blank=True,
help_text="Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}",
validators=[passerelle.utils.templates.validate_template],
verbose_name='Id template',
),
),
(
'text_template',
models.TextField(
blank=True,
help_text="Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}",
validators=[passerelle.utils.templates.validate_template],
verbose_name='Text template',
),
),
(
'resource',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='arcgis.ArcGIS',
verbose_name='Resource',
),
),
], ],
options={ options={
'ordering': ['name'], 'ordering': ['name'],

View File

@ -16,7 +16,9 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='arcgis', model_name='arcgis',
name='client_certificate', name='client_certificate',
field=models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS client certificate'), field=models.FileField(
blank=True, null=True, upload_to='', verbose_name='TLS client certificate'
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='arcgis', model_name='arcgis',
@ -26,6 +28,11 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='query', model_name='query',
name='resource', name='resource',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='queries', to='arcgis.ArcGIS', verbose_name='Resource'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='queries',
to='arcgis.ArcGIS',
verbose_name='Resource',
),
), ),
] ]

View File

@ -46,44 +46,61 @@ class ArcGIS(BaseResource, HTTPResource):
class Meta: class Meta:
verbose_name = _('ArcGIS REST API') verbose_name = _('ArcGIS REST API')
@endpoint(name='mapservice-query', @endpoint(
description=_('Map Service Query'), name='mapservice-query',
perm='can_access', description=_('Map Service Query'),
parameters={ perm='can_access',
'folder': { parameters={
'description': _('Folder name'), 'folder': {
'example_value': 'Specialty', 'description': _('Folder name'),
}, 'example_value': 'Specialty',
'service': { },
'description': _('Service name'), 'service': {
'example_value': 'ESRI_StateCityHighway_USA', 'description': _('Service name'),
}, 'example_value': 'ESRI_StateCityHighway_USA',
'layer': { },
'description': _('Layer or table name'), 'layer': {
'example_value': '1', 'description': _('Layer or table name'),
}, 'example_value': '1',
'lat': {'description': _('Latitude')}, },
'lon': {'description': _('Longitude')}, 'lat': {'description': _('Latitude')},
'latmin': {'description': _('Minimal latitude (envelope)')}, 'lon': {'description': _('Longitude')},
'lonmin': {'description': _('Minimal longitude (envelope)')}, 'latmin': {'description': _('Minimal latitude (envelope)')},
'latmax': {'description': _('Maximal latitude (envelope)')}, 'lonmin': {'description': _('Minimal longitude (envelope)')},
'lonmax': {'description': _('Maximal longitude (envelope)')}, 'latmax': {'description': _('Maximal latitude (envelope)')},
'q': {'description': _('Search text in display field')}, 'lonmax': {'description': _('Maximal longitude (envelope)')},
'template': { 'q': {'description': _('Search text in display field')},
'description': _('Django template for text attribute'), 'template': {
'example_value': '{{ attributes.STATE_NAME }} ({{ attributes.STATE_ABBR }})', 'description': _('Django template for text attribute'),
}, 'example_value': '{{ attributes.STATE_NAME }} ({{ attributes.STATE_ABBR }})',
'id_template': { },
'description': _('Django template for id attribute'), 'id_template': {
}, 'description': _('Django template for id attribute'),
'full': { },
'description': _('Returns all ArcGIS informations (geometry, metadata)'), 'full': {
'type': 'bool', 'description': _('Returns all ArcGIS informations (geometry, metadata)'),
}, 'type': 'bool',
}) },
def mapservice_query(self, request, service, layer='0', folder='', lat=None, lon=None, },
latmin=None, lonmin=None, latmax=None, lonmax=None, q=None, )
template=None, id_template=None, full=False, **kwargs): def mapservice_query(
self,
request,
service,
layer='0',
folder='',
lat=None,
lon=None,
latmin=None,
lonmin=None,
latmax=None,
lonmax=None,
q=None,
template=None,
id_template=None,
full=False,
**kwargs,
):
url = urlparse.urljoin(self.base_url, 'services/') url = urlparse.urljoin(self.base_url, 'services/')
if folder: if folder:
url = urlparse.urljoin(url, folder + '/') url = urlparse.urljoin(url, folder + '/')
@ -109,8 +126,7 @@ class ArcGIS(BaseResource, HTTPResource):
lonmin, latmin = float(lonmin), float(latmin) lonmin, latmin = float(lonmin), float(latmin)
lonmax, latmax = float(lonmax), float(latmax) lonmax, latmax = float(lonmax), float(latmax)
except (ValueError,): except (ValueError,):
raise APIError('<lonmin> <latmin> <lonmax> and <latmax> must be floats', raise APIError('<lonmin> <latmin> <lonmax> and <latmax> must be floats', http_status=400)
http_status=400)
params['geometry'] = '{},{},{},{}'.format(lonmin, latmin, lonmax, latmax) params['geometry'] = '{},{},{},{}'.format(lonmin, latmin, lonmax, latmax)
params['geometryType'] = 'esriGeometryEnvelope' params['geometryType'] = 'esriGeometryEnvelope'
if q is not None: if q is not None:
@ -156,7 +172,7 @@ class ArcGIS(BaseResource, HTTPResource):
feature['id'] = '%s' % get_feature_attribute(feature, id_fieldname) feature['id'] = '%s' % get_feature_attribute(feature, id_fieldname)
feature['text'] = '%s' % get_feature_attribute(feature, text_fieldname) feature['text'] = '%s' % get_feature_attribute(feature, text_fieldname)
else: else:
feature['id'] = feature['text'] = '%d' % (n+1) feature['id'] = feature['text'] = '%d' % (n + 1)
if template: if template:
feature['text'] = render_to_string(template, feature) feature['text'] = render_to_string(template, feature)
if id_template: if id_template:
@ -169,22 +185,30 @@ class ArcGIS(BaseResource, HTTPResource):
return {'data': data, 'metadata': infos} return {'data': data, 'metadata': infos}
return {'data': data} return {'data': data}
@endpoint(name='district', @endpoint(
description=_('Districts in Nancy Town'), name='district',
parameters={ description=_('Districts in Nancy Town'),
'lat': {'description': _('Latitude')}, parameters={
'lon': {'description': _('Longitude')}, 'lat': {'description': _('Latitude')},
}, 'lon': {'description': _('Longitude')},
show=False) },
show=False,
)
def district(self, request, lon=None, lat=None): def district(self, request, lon=None, lat=None):
# deprecated endpoint # deprecated endpoint
if 'NANCY_Grc' in self.base_url: if 'NANCY_Grc' in self.base_url:
# Nancy URL used to contains folder, service and layer, remove them # Nancy URL used to contains folder, service and layer, remove them
self.base_url = 'https://geoservices.grand-nancy.org/arcgis/rest/' self.base_url = 'https://geoservices.grand-nancy.org/arcgis/rest/'
features = self.mapservice_query(request, folder='public', service='NANCY_Grc', layer='0', features = self.mapservice_query(
template='{{ attributes.NOM }}', request,
id_template='{{ attributes.NUMERO }}', folder='public',
lon=lon, lat=lat)['data'] service='NANCY_Grc',
layer='0',
template='{{ attributes.NOM }}',
id_template='{{ attributes.NUMERO }}',
lon=lon,
lat=lat,
)['data']
if not features: if not features:
raise APIError('No features found.') raise APIError('No features found.')
for feature in features: for feature in features:
@ -197,15 +221,14 @@ class ArcGIS(BaseResource, HTTPResource):
@endpoint( @endpoint(
name='tile', name='tile',
description=_('Tiles layer'), description=_('Tiles layer'),
pattern=r'^(?P<layer>[\w/]+)/(?P<zoom>\d+)/(?P<tile_x>\d+)/(?P<tile_y>\d+)\.png$') pattern=r'^(?P<layer>[\w/]+)/(?P<zoom>\d+)/(?P<tile_x>\d+)/(?P<tile_y>\d+)\.png$',
)
def tile(self, request, layer, zoom, tile_x, tile_y): def tile(self, request, layer, zoom, tile_x, tile_y):
zoom = int(zoom) zoom = int(zoom)
tile_x = int(tile_x) tile_x = int(tile_x)
tile_y = int(tile_y) tile_y = int(tile_y)
bbox = '%.6f,%.6f,%.6f,%.6f' % ( bbox = '%.6f,%.6f,%.6f,%.6f' % (num2deg(tile_x, tile_y, zoom) + num2deg(tile_x + 1, tile_y + 1, zoom))
num2deg(tile_x, tile_y, zoom) +
num2deg(tile_x+1, tile_y+1, zoom))
# imageSR=3857: default projection for leaflet # imageSR=3857: default projection for leaflet
base_url = self.base_url base_url = self.base_url
@ -213,19 +236,22 @@ class ArcGIS(BaseResource, HTTPResource):
base_url += '/' base_url += '/'
return HttpResponse( return HttpResponse(
self.requests.get( self.requests.get(
base_url + base_url
'%s/MapServer/export' % layer + + '%s/MapServer/export' % layer
'?dpi=96&format=png24&bboxSR=4326&imageSR=3857&' + + '?dpi=96&format=png24&bboxSR=4326&imageSR=3857&'
'transparent=true&size=256,256&f=image&' + + 'transparent=true&size=256,256&f=image&'
'bbox=%s' % bbox + 'bbox=%s' % bbox
).content, ).content,
content_type='image/png') content_type='image/png',
)
@endpoint(name='q', @endpoint(
description=_('Query'), name='q',
pattern=r'^(?P<query_slug>[\w:_-]+)/$', description=_('Query'),
perm='can_access', pattern=r'^(?P<query_slug>[\w:_-]+)/$',
show=False) perm='can_access',
show=False,
)
def q(self, request, query_slug, q=None, full=False, **kwargs): def q(self, request, query_slug, q=None, full=False, **kwargs):
query = get_object_or_404(Query, resource=self, slug=query_slug) query = get_object_or_404(Query, resource=self, slug=query_slug)
refs = [ref for ref, _ in query.where_references] refs = [ref for ref, _ in query.where_references]
@ -282,22 +308,12 @@ def validate_where(format_string):
class Query(BaseQuery): class Query(BaseQuery):
resource = models.ForeignKey( resource = models.ForeignKey(
to=ArcGIS, to=ArcGIS, related_name='queries', verbose_name=_('Resource'), on_delete=models.CASCADE
related_name='queries', )
verbose_name=_('Resource'),
on_delete=models.CASCADE)
folder = models.CharField( folder = models.CharField(verbose_name=_('ArcGis Folder'), max_length=64, blank=True)
verbose_name=_('ArcGis Folder'), service = models.CharField(verbose_name=_('ArcGis Service'), max_length=64)
max_length=64, layer = models.CharField(verbose_name=_('ArcGis Layer'), max_length=8, blank=True)
blank=True)
service = models.CharField(
verbose_name=_('ArcGis Service'),
max_length=64)
layer = models.CharField(
verbose_name=_('ArcGis Layer'),
max_length=8,
blank=True)
where = models.TextField( where = models.TextField(
verbose_name=_('ArcGis Where Clause'), verbose_name=_('ArcGis Where Clause'),
@ -308,19 +324,28 @@ class Query(BaseQuery):
'<span>Use syntax <tt>{name}</tt> to introduce a string ' '<span>Use syntax <tt>{name}</tt> to introduce a string '
'parameter and <tt>{name:d}</tt> for a decimal parameter. ex.:<br/>' 'parameter and <tt>{name:d}</tt> for a decimal parameter. ex.:<br/>'
'<tt>adress LIKE (\'%\' || UPPER({adress}) || \'%\')</tt><br/>' '<tt>adress LIKE (\'%\' || UPPER({adress}) || \'%\')</tt><br/>'
'<tt>population < {population:d}</tt></span>'))) '<tt>population < {population:d}</tt></span>'
)
),
)
id_template = models.TextField( id_template = models.TextField(
verbose_name=_('Id template'), verbose_name=_('Id template'),
validators=[validate_template], validators=[validate_template],
help_text=_('Use Django\'s template syntax. Attributes can be accessed through {{ attributes.name }}'), help_text=_(
blank=True) 'Use Django\'s template syntax. Attributes can be accessed through {{ attributes.name }}'
),
blank=True,
)
text_template = models.TextField( text_template = models.TextField(
verbose_name=_('Text template'), verbose_name=_('Text template'),
help_text=_('Use Django\'s template syntax. Attributes can be accessed through {{ attributes.name }}'), help_text=_(
'Use Django\'s template syntax. Attributes can be accessed through {{ attributes.name }}'
),
validators=[validate_template], validators=[validate_template],
blank=True) blank=True,
)
delete_view = 'arcgis-query-delete' delete_view = 'arcgis-query-delete'
edit_view = 'arcgis-query-edit' edit_view = 'arcgis-query-edit'
@ -328,15 +353,20 @@ class Query(BaseQuery):
@property @property
def where_references(self): def where_references(self):
if self.where: if self.where:
return [(ref, int if spec and spec[-1] == 'd' else str) return [
for _, ref, spec, _ in SqlFormatter().parse(self.where) if ref is not None] (ref, int if spec and spec[-1] == 'd' else str)
for _, ref, spec, _ in SqlFormatter().parse(self.where)
if ref is not None
]
else: else:
return [] return []
def q(self, request, q=None, full=False, **kwargs): def q(self, request, q=None, full=False, **kwargs):
kwargs.update({ kwargs.update(
'service': self.service, {
}) 'service': self.service,
}
)
if self.id_template: if self.id_template:
kwargs['id_template'] = self.id_template kwargs['id_template'] = self.id_template
if self.text_template: if self.text_template:

View File

@ -19,10 +19,11 @@ from django.conf.urls import url
from . import views from . import views
management_urlpatterns = [ management_urlpatterns = [
url(r'^(?P<slug>[\w,-]+)/query/new/$', url(r'^(?P<slug>[\w,-]+)/query/new/$', views.QueryNew.as_view(), name='arcgis-query-new'),
views.QueryNew.as_view(), name='arcgis-query-new'), url(r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/$', views.QueryEdit.as_view(), name='arcgis-query-edit'),
url(r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/$', url(
views.QueryEdit.as_view(), name='arcgis-query-edit'), r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/delete/$',
url(r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/delete/$', views.QueryDelete.as_view(),
views.QueryDelete.as_view(), name='arcgis-query-delete'), name='arcgis-query-delete',
),
] ]

View File

@ -14,15 +14,41 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='ArpegeECP', name='ArpegeECP',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('log_level', models.CharField(default=b'INFO', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')])), (
'log_level',
models.CharField(
default=b'INFO',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
),
('webservice_base_url', models.URLField(verbose_name='Webservice Base URL')), ('webservice_base_url', models.URLField(verbose_name='Webservice Base URL')),
('hawk_auth_id', models.CharField(max_length=64, verbose_name='Hawk Authentication id')), ('hawk_auth_id', models.CharField(max_length=64, verbose_name='Hawk Authentication id')),
('hawk_auth_key', models.CharField(max_length=64, verbose_name='Hawk Authentication secret')), ('hawk_auth_key', models.CharField(max_length=64, verbose_name='Hawk Authentication secret')),
('users', models.ManyToManyField(to='base.ApiUser', related_name='_arpegeecp_users_+', related_query_name='+', blank=True)), (
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_arpegeecp_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'verbose_name': 'Arpege ECP', 'verbose_name': 'Arpege ECP',

View File

@ -56,8 +56,9 @@ class ArpegeECP(BaseResource):
def get_access_token(self, NameID): def get_access_token(self, NameID):
url = urlparse.urljoin(self.webservice_base_url, 'LoginParSubOIDC') url = urlparse.urljoin(self.webservice_base_url, 'LoginParSubOIDC')
try: try:
response = self.requests.post(url, auth=HawkAuth(self.hawk_auth_id, self.hawk_auth_key), response = self.requests.post(
json={'subOIDC': NameID}) url, auth=HawkAuth(self.hawk_auth_id, self.hawk_auth_key), json={'subOIDC': NameID}
)
response.raise_for_status() response.raise_for_status()
except RequestException as e: except RequestException as e:
raise APIError(u'Arpege server is down: %s' % e) raise APIError(u'Arpege server is down: %s' % e)
@ -73,7 +74,12 @@ class ArpegeECP(BaseResource):
return result['Data']['AccessToken'] return result['Data']['AccessToken']
raise APIError(u'%s (%s)' % (result.get('LibErreur'), result.get('CodErreur'))) raise APIError(u'%s (%s)' % (result.get('LibErreur'), result.get('CodErreur')))
@endpoint(name='api', pattern='^users/(?P<nameid>\w+)/forms$', perm='can_access', description='Returns user forms') @endpoint(
name='api',
pattern='^users/(?P<nameid>\w+)/forms$',
perm='can_access',
description='Returns user forms',
)
def get_user_forms(self, request, nameid): def get_user_forms(self, request, nameid):
access_token = self.get_access_token(nameid) access_token = self.get_access_token(nameid)
url = urlparse.urljoin(self.webservice_base_url, 'DemandesUsager') url = urlparse.urljoin(self.webservice_base_url, 'DemandesUsager')
@ -98,14 +104,15 @@ class ArpegeECP(BaseResource):
receipt_date = parse_date(data_administratives['date_depot']) receipt_date = parse_date(data_administratives['date_depot'])
except (KeyError, TypeError) as e: except (KeyError, TypeError) as e:
raise APIError(u'Arpege error: %s %r' % (e, json.dumps(demand)[:1000])) raise APIError(u'Arpege error: %s %r' % (e, json.dumps(demand)[:1000]))
d = {'url': demand['url'], d = {
'title': data_administratives.get('LibelleQualificationTypeDemande'), 'url': demand['url'],
'name': data_administratives.get('LibelleQualificationTypeDemande'), 'title': data_administratives.get('LibelleQualificationTypeDemande'),
'status': data_administratives.get('libelle_etat'), 'name': data_administratives.get('LibelleQualificationTypeDemande'),
'form_receipt_time': receipt_time, 'status': data_administratives.get('libelle_etat'),
'readable': True, 'form_receipt_time': receipt_time,
'form_receipt_datetime': timezone.datetime.combine(receipt_date, receipt_time), 'readable': True,
'form_status_is_endpoint': data_administratives.get('date_fin_instruction') is not None, 'form_receipt_datetime': timezone.datetime.combine(receipt_date, receipt_time),
'form_status_is_endpoint': data_administratives.get('date_fin_instruction') is not None,
} }
data.append(d) data.append(d)
return {'data': data} return {'data': data}

View File

@ -18,7 +18,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='AstreGS', name='AstreGS',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('slug', models.SlugField(unique=True, verbose_name='Identifier')), ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
@ -28,7 +31,12 @@ class Migration(migrations.Migration):
('organism', models.CharField(max_length=32, verbose_name='Organisme')), ('organism', models.CharField(max_length=32, verbose_name='Organisme')),
('budget', models.CharField(max_length=32, verbose_name='Budget')), ('budget', models.CharField(max_length=32, verbose_name='Budget')),
('exercice', models.CharField(max_length=32, verbose_name='Exercice')), ('exercice', models.CharField(max_length=32, verbose_name='Exercice')),
('users', models.ManyToManyField(blank=True, related_name='_astregs_users_+', related_query_name='+', to='base.ApiUser')), (
'users',
models.ManyToManyField(
blank=True, related_name='_astregs_users_+', related_query_name='+', to='base.ApiUser'
),
),
], ],
options={ options={
'verbose_name': 'AstresGS', 'verbose_name': 'AstresGS',
@ -37,11 +45,17 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Link', name='Link',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name_id', models.CharField(max_length=32)), ('name_id', models.CharField(max_length=32)),
('association_id', models.CharField(max_length=32)), ('association_id', models.CharField(max_length=32)),
('created', models.DateTimeField(auto_now_add=True)), ('created', models.DateTimeField(auto_now_add=True)),
('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='astregs.AstreGS')), (
'resource',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='astregs.AstreGS'),
),
], ],
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(

View File

@ -46,18 +46,35 @@ ASSOCIATION_SCHEMA = {
], ],
"properties": { "properties": {
"Financier": {"description": "financial association", "type": "string", "enum": ["true", "false"]}, "Financier": {"description": "financial association", "type": "string", "enum": ["true", "false"]},
"CodeFamille": {"description": "association family code", "type": "string",}, "CodeFamille": {
"CatTiers": {"description": "association category", "type": "string",}, "description": "association family code",
"NomEnregistrement": {"description": "association name", "type": "string",}, "type": "string",
},
"CatTiers": {
"description": "association category",
"type": "string",
},
"NomEnregistrement": {
"description": "association name",
"type": "string",
},
"StatutTiers": { "StatutTiers": {
"description": "association status", "description": "association status",
"type": "string", "type": "string",
"enum": ["PROPOSE", "VALIDE", "REFUSE", "BLOQUE", "A COMPLETER"], "enum": ["PROPOSE", "VALIDE", "REFUSE", "BLOQUE", "A COMPLETER"],
}, },
"Type": {"description": "association type", "type": "string", "enum": ["D", "F", "*"]}, "Type": {"description": "association type", "type": "string", "enum": ["D", "F", "*"]},
"NumeroSiret": {"description": "SIREN number", "type": "string",}, "NumeroSiret": {
"NumeroSiretFin": {"description": "NIC number", "type": "string",}, "description": "SIREN number",
"AdresseTitre": {"type": "string",}, "type": "string",
},
"NumeroSiretFin": {
"description": "NIC number",
"type": "string",
},
"AdresseTitre": {
"type": "string",
},
"AdresseIsAdresseDeCommande": {"type": "string", "enum": ["true", "false"]}, "AdresseIsAdresseDeCommande": {"type": "string", "enum": ["true", "false"]},
"AdresseIsAdresseDeFacturation": {"type": "string", "enum": ["true", "false"]}, "AdresseIsAdresseDeFacturation": {"type": "string", "enum": ["true", "false"]},
}, },
@ -78,13 +95,27 @@ CONTACT_SCHEMA = {
"EncodeKeyStatut", "EncodeKeyStatut",
], ],
"properties": { "properties": {
"CodeContact": {"type": "string",}, "CodeContact": {
"CodeTitreCivilite": {"type": "string",}, "type": "string",
"Nom": {"type": "string",}, },
"AdresseDestinataire": {"type": "string",}, "CodeTitreCivilite": {
"CodePostal": {"type": "string",}, "type": "string",
"Ville": {"type": "string",}, },
"EncodeKeyStatut": {"type": "string",}, "Nom": {
"type": "string",
},
"AdresseDestinataire": {
"type": "string",
},
"CodePostal": {
"type": "string",
},
"Ville": {
"type": "string",
},
"EncodeKeyStatut": {
"type": "string",
},
}, },
} }
@ -105,21 +136,43 @@ DOCUMENT_SCHEMA = {
"document", "document",
], ],
"properties": { "properties": {
"Sujet": {"type": "string",}, "Sujet": {
"Entite": {"type": "string",}, "type": "string",
"CodType": {"type": "string",}, },
"Type": {"type": "string",}, "Entite": {
"hdnCodeTrt": {"type": "string",}, "type": "string",
"EncodeKeyEntite": {"type": "string",}, },
"CodeDomaine": {"type": "string",}, "CodType": {
"CodDom": {"type": "string",}, "type": "string",
},
"Type": {
"type": "string",
},
"hdnCodeTrt": {
"type": "string",
},
"EncodeKeyEntite": {
"type": "string",
},
"CodeDomaine": {
"type": "string",
},
"CodDom": {
"type": "string",
},
"document": { "document": {
"type": "object", "type": "object",
"required": ['filename', 'content_type', 'content'], "required": ['filename', 'content_type', 'content'],
'properties': { 'properties': {
'filename': {'type': 'string',}, 'filename': {
'content_type': {'type': 'string',}, 'type': 'string',
'content': {'type': 'string',}, },
'content_type': {
'type': 'string',
},
'content': {
'type': 'string',
},
}, },
}, },
}, },
@ -141,14 +194,28 @@ GRANT_SCHEMA = {
"CodeServiceUtilisateur", "CodeServiceUtilisateur",
], ],
"properties": { "properties": {
"Libelle": {"type": "string",}, "Libelle": {
"LibelleCourt": {"type": "string",}, "type": "string",
},
"LibelleCourt": {
"type": "string",
},
"ModGestion": {"type": "string", "enum": ["1", "2", "3", "4"]}, "ModGestion": {"type": "string", "enum": ["1", "2", "3", "4"]},
"TypeAide": {"type": "string",}, "TypeAide": {
"Sens": {"type": "string",}, "type": "string",
"CodeTiersDem": {"type": "string",}, },
"CodeServiceGestionnaire": {"type": "string",}, "Sens": {
"CodeServiceUtilisateur": {"type": "string",}, "type": "string",
},
"CodeTiersDem": {
"type": "string",
},
"CodeServiceGestionnaire": {
"type": "string",
},
"CodeServiceUtilisateur": {
"type": "string",
},
}, },
} }
@ -159,11 +226,21 @@ INDANA_SCHEMA = {
"type": "object", "type": "object",
"required": ["CodeDossier", "CodeInd_1", "AnneeInd_1", "ValInd_1"], "required": ["CodeDossier", "CodeInd_1", "AnneeInd_1", "ValInd_1"],
"properties": { "properties": {
"CodeDossier": {"type": "string",}, "CodeDossier": {
"CodeInd_1": {"type": "string",}, "type": "string",
"AnneeInd_1": {"type": "string",}, },
"ValInd_1": {"type": "string",}, "CodeInd_1": {
"IndAide": {"type": "string",}, "type": "string",
},
"AnneeInd_1": {
"type": "string",
},
"ValInd_1": {
"type": "string",
},
"IndAide": {
"type": "string",
},
}, },
} }
@ -174,9 +251,15 @@ INDANA_KEY_SCHEMA = {
"type": "object", "type": "object",
"required": ["CodeDossier", "CodeInd_1", "AnneeInd_1"], "required": ["CodeDossier", "CodeInd_1", "AnneeInd_1"],
"properties": { "properties": {
"CodeDossier": {"type": "string",}, "CodeDossier": {
"CodeInd_1": {"type": "string",}, "type": "string",
"AnneeInd_1": {"type": "string",}, },
"CodeInd_1": {
"type": "string",
},
"AnneeInd_1": {
"type": "string",
},
}, },
} }
@ -197,51 +280,26 @@ TIERS_RIB_SCHEMA = {
"CodeStatut", "CodeStatut",
"CodeDevise", "CodeDevise",
"CodeIso2Pays", "CodeIso2Pays",
"LibelleCompteEtranger" "LibelleCompteEtranger",
], ],
"properties": { "properties": {
"CodeDevise": { "CodeDevise": {"type": "string"},
"type": "string" "CodeDomiciliation": {"type": "string"},
}, "CodeIso2Pays": {"type": "string"},
"CodeDomiciliation": { "CodePaiement": {"type": "string"},
"type": "string"
},
"CodeIso2Pays": {
"type": "string"
},
"CodePaiement": {
"type": "string"
},
"CodeStatut": { "CodeStatut": {
"type": "string", "type": "string",
"enum": ["PROPOSE", "VALIDE", "REFUSE", "A COMPLETER", "enum": ["PROPOSE", "VALIDE", "REFUSE", "A COMPLETER", "BLOQUE", "EN MODIFICATION"],
"BLOQUE", "EN MODIFICATION"]
}, },
"CodeTiers": { "CodeTiers": {"type": "string"},
"type": "string" "IndicateurRibDefaut": {"type": "string"},
}, "LibelleCompteEtranger": {"type": "string"},
"IndicateurRibDefaut": { "LibelleCourt": {"type": "string"},
"type": "string" "NumeroIban": {"type": "string"},
}, "CleIban": {"type": "string"},
"LibelleCompteEtranger": { "CodeBic": {"type": "string"},
"type": "string" "IdRib": {"type": "string"},
}, },
"LibelleCourt": {
"type": "string"
},
"NumeroIban": {
"type": "string"
},
"CleIban": {
"type": "string"
},
"CodeBic": {
"type": "string"
},
"IdRib": {
"type": "string"
}
}
} }
TIERS_RIB_UPDATE_SCHEMA = { TIERS_RIB_UPDATE_SCHEMA = {
@ -259,45 +317,24 @@ TIERS_RIB_UPDATE_SCHEMA = {
"CodeStatut", "CodeStatut",
"CodeDevise", "CodeDevise",
"CodeIso2Pays", "CodeIso2Pays",
"LibelleCompteEtranger" "LibelleCompteEtranger",
], ],
"properties": { "properties": {
"CodeDevise": { "CodeDevise": {"type": "string"},
"type": "string" "CodeDomiciliation": {"type": "string"},
}, "CodeIso2Pays": {"type": "string"},
"CodeDomiciliation": { "CodePaiement": {"type": "string"},
"type": "string"
},
"CodeIso2Pays": {
"type": "string"
},
"CodePaiement": {
"type": "string"
},
"CodeStatut": { "CodeStatut": {
"type": "string", "type": "string",
"enum": ["PROPOSE", "VALIDE", "REFUSE", "A COMPLETER", "enum": ["PROPOSE", "VALIDE", "REFUSE", "A COMPLETER", "BLOQUE", "EN MODIFICATION"],
"BLOQUE", "EN MODIFICATION"]
}, },
"IndicateurRibDefaut": { "IndicateurRibDefaut": {"type": "string"},
"type": "string" "LibelleCompteEtranger": {"type": "string"},
}, "LibelleCourt": {"type": "string"},
"LibelleCompteEtranger": { "NumeroIban": {"type": "string"},
"type": "string" "CleIban": {"type": "string"},
}, "CodeBic": {"type": "string"},
"LibelleCourt": { },
"type": "string"
},
"NumeroIban": {
"type": "string"
},
"CleIban": {
"type": "string"
},
"CodeBic": {
"type": "string"
}
}
} }
@ -434,8 +471,14 @@ class AstreGS(BaseResource):
description=_('Create link between user and association'), description=_('Create link between user and association'),
perm='can_access', perm='can_access',
parameters={ parameters={
'NameID': {'description': _('Publik NameID'), 'example_value': 'xyz24d934',}, 'NameID': {
'association_id': {'description': _('Association ID'), 'example_value': '12345',}, 'description': _('Publik NameID'),
'example_value': 'xyz24d934',
},
'association_id': {
'description': _('Association ID'),
'example_value': '12345',
},
}, },
) )
def link(self, request, NameID, association_id): def link(self, request, NameID, association_id):
@ -464,7 +507,12 @@ class AstreGS(BaseResource):
@endpoint( @endpoint(
description=_('List user links'), description=_('List user links'),
perm='can_access', perm='can_access',
parameters={'NameID': {'description': _('Publik NameID'), 'example_value': 'xyz24d934',}}, parameters={
'NameID': {
'description': _('Publik NameID'),
'example_value': 'xyz24d934',
}
},
) )
def links(self, request, NameID): def links(self, request, NameID):
if not Link.objects.filter(resource=self, name_id=NameID).exists(): if not Link.objects.filter(resource=self, name_id=NameID).exists():
@ -508,7 +556,12 @@ class AstreGS(BaseResource):
name='get-contact', name='get-contact',
perm='can_access', perm='can_access',
description=_('Get contact details'), description=_('Get contact details'),
parameters={'contact_id': {'description': _('Contact identifier'), 'example_value': '1111',}}, parameters={
'contact_id': {
'description': _('Contact identifier'),
'example_value': '1111',
}
},
) )
def get_contact(self, request, contact_id): def get_contact(self, request, contact_id):
r = self.call('Contact', 'Chargement', ContactCle={'idContact': contact_id}) r = self.call('Contact', 'Chargement', ContactCle={'idContact': contact_id})
@ -533,7 +586,9 @@ class AstreGS(BaseResource):
description=_('Delete contact'), description=_('Delete contact'),
name='delete-contact', name='delete-contact',
perm='can_access', perm='can_access',
parameters={'contact_id': {'description': _('Contact ID'), 'example_value': '4242'},}, parameters={
'contact_id': {'description': _('Contact ID'), 'example_value': '4242'},
},
) )
def delete_contact(self, request, contact_id): def delete_contact(self, request, contact_id):
r = self.call('Contact', 'Suppression', ContactCle={'idContact': contact_id}) r = self.call('Contact', 'Suppression', ContactCle={'idContact': contact_id})
@ -615,31 +670,31 @@ class AstreGS(BaseResource):
r = self.call('TiersRib', 'Creation', TiersRib=post_data) r = self.call('TiersRib', 'Creation', TiersRib=post_data)
return {'data': serialize_object(r)} return {'data': serialize_object(r)}
@endpoint( @endpoint(
name='get-tiers-rib', perm='can_access', name='get-tiers-rib',
perm='can_access',
description=_('Get RIB'), description=_('Get RIB'),
parameters={ parameters={
'CodeTiers': {'example_value': '42435'}, 'CodeTiers': {'example_value': '42435'},
'IdRib': {'example_value': '4242'}, 'IdRib': {'example_value': '4242'},
} },
) )
def get_tiers_rib(self, request, CodeTiers, IdRib): def get_tiers_rib(self, request, CodeTiers, IdRib):
payload = {'CodeTiers': CodeTiers, 'IdRib': IdRib} payload = {'CodeTiers': CodeTiers, 'IdRib': IdRib}
r = self.call('TiersRib', 'Chargement', TiersRibCle=payload) r = self.call('TiersRib', 'Chargement', TiersRibCle=payload)
return {'data': serialize_object(r)} return {'data': serialize_object(r)}
@endpoint( @endpoint(
name='update-tiers-rib', perm='can_access', name='update-tiers-rib',
perm='can_access',
post={ post={
'description': _('Update RIB'), 'description': _('Update RIB'),
'request_body': {'schema': {'application/json': TIERS_RIB_UPDATE_SCHEMA}} 'request_body': {'schema': {'application/json': TIERS_RIB_UPDATE_SCHEMA}},
}, },
parameters={ parameters={
'CodeTiers': {'example_value': '42435'}, 'CodeTiers': {'example_value': '42435'},
'IdRib': {'example_value': '4242'}, 'IdRib': {'example_value': '4242'},
} },
) )
def update_tiers_rib(self, request, CodeTiers, IdRib, post_data): def update_tiers_rib(self, request, CodeTiers, IdRib, post_data):
post_data['CodeTiers'] = CodeTiers post_data['CodeTiers'] = CodeTiers
@ -647,32 +702,33 @@ class AstreGS(BaseResource):
r = self.call('TiersRib', 'Modification', TiersRib=post_data) r = self.call('TiersRib', 'Modification', TiersRib=post_data)
return {'data': serialize_object(r)} return {'data': serialize_object(r)}
@endpoint(
@endpoint(name='delete-tiers-rib', perm='can_access', name='delete-tiers-rib',
perm='can_access',
description=_('Delete RIB'), description=_('Delete RIB'),
parameters={ parameters={
'CodeTiers': {'example_value': '42435'}, 'CodeTiers': {'example_value': '42435'},
'IdRib': {'example_value': '4242'}, 'IdRib': {'example_value': '4242'},
} },
) )
def delete_tiers_rib(self, request, CodeTiers, IdRib): def delete_tiers_rib(self, request, CodeTiers, IdRib):
payload = {'CodeTiers': CodeTiers, 'IdRib': IdRib} payload = {'CodeTiers': CodeTiers, 'IdRib': IdRib}
r = self.call('TiersRib', 'Suppression', TiersRibCle=payload) r = self.call('TiersRib', 'Suppression', TiersRibCle=payload)
return {'data': serialize_object(r)} return {'data': serialize_object(r)}
@endpoint(name='find-tiers-by-rib', perm='can_access', @endpoint(
name='find-tiers-by-rib',
perm='can_access',
description=_('Find person by RIB'), description=_('Find person by RIB'),
parameters={ parameters={
'banque': {'example_value': '30001'}, 'banque': {'example_value': '30001'},
'guichet': {'example_value': '00794'}, 'guichet': {'example_value': '00794'},
'numero_compte': {'example_value': '12345678901'}, 'numero_compte': {'example_value': '12345678901'},
'cle': {'example_value': '85'}, 'cle': {'example_value': '85'},
} },
) )
def find_tiers_by_rib(self, request, banque, guichet, numero_compte, cle, **kwargs): def find_tiers_by_rib(self, request, banque, guichet, numero_compte, cle, **kwargs):
criteres = {'banque': banque, 'guichet': guichet, criteres = {'banque': banque, 'guichet': guichet, 'numeroCompte': numero_compte, 'cleRIB': cle}
'numeroCompte': numero_compte,
'cleRIB': cle}
# add other params to search criterias # add other params to search criterias
criteres.update(kwargs) criteres.update(kwargs)
r = self.search_tiers(criteres) r = self.search_tiers(criteres)

View File

@ -17,12 +17,30 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='ATALConnector', name='ATALConnector',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('slug', models.SlugField(unique=True, verbose_name='Identifier')), ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
('base_soap_url', models.URLField(help_text='URL of the base SOAP endpoint', max_length=400, verbose_name='Base SOAP endpoint')), (
('users', models.ManyToManyField(blank=True, related_name='_atalconnector_users_+', related_query_name='+', to='base.ApiUser')), 'base_soap_url',
models.URLField(
help_text='URL of the base SOAP endpoint',
max_length=400,
verbose_name='Base SOAP endpoint',
),
),
(
'users',
models.ManyToManyField(
blank=True,
related_name='_atalconnector_users_+',
related_query_name='+',
to='base.ApiUser',
),
),
], ],
options={ options={
'verbose_name': 'ATAL connector', 'verbose_name': 'ATAL connector',

View File

@ -43,17 +43,14 @@ def process_response(demande_number):
class ATALConnector(BaseResource): class ATALConnector(BaseResource):
base_soap_url = models.URLField( base_soap_url = models.URLField(
max_length=400, verbose_name=_('Base SOAP endpoint'), max_length=400, verbose_name=_('Base SOAP endpoint'), help_text=_('URL of the base SOAP endpoint')
help_text=_('URL of the base SOAP endpoint')) )
category = _('Business Process Connectors') category = _('Business Process Connectors')
class Meta: class Meta:
verbose_name = _('ATAL connector') verbose_name = _('ATAL connector')
DEMANDE_NUMBER_PARAM = { DEMANDE_NUMBER_PARAM = {'description': _('Demande number'), 'example_value': 'DIT18050001'}
'description': _('Demande number'),
'example_value': 'DIT18050001'
}
def _soap_call(self, wsdl, method, **kwargs): def _soap_call(self, wsdl, method, **kwargs):
wsdl_url = urllib.parse.urljoin(self.base_soap_url, '%s?wsdl' % wsdl) wsdl_url = urllib.parse.urljoin(self.base_soap_url, '%s?wsdl' % wsdl)
@ -96,34 +93,29 @@ class ATALConnector(BaseResource):
return self._xml_ref('VilleAgileService', 'getTypesEquipement', 'types') return self._xml_ref('VilleAgileService', 'getTypesEquipement', 'types')
@endpoint( @endpoint(
perm='can_access', name='insert-action-comment', perm='can_access',
name='insert-action-comment',
post={ post={
'description': _('Insert action comment'), 'description': _('Insert action comment'),
'request_body': { 'request_body': {'schema': {'application/json': schemas.INSERT_ACTION_COMMENT}},
'schema': { },
'application/json': schemas.INSERT_ACTION_COMMENT
}
}
}
) )
def insert_action_comment(self, request, post_data): def insert_action_comment(self, request, post_data):
demande_number = self._soap_call( demande_number = self._soap_call(
wsdl='DemandeService', method='insertActionComment', wsdl='DemandeService',
method='insertActionComment',
numeroDemande=post_data['numero_demande'], numeroDemande=post_data['numero_demande'],
commentaire=post_data['commentaire'] commentaire=post_data['commentaire'],
) )
return process_response(demande_number) return process_response(demande_number)
@endpoint( @endpoint(
perm='can_access', name='insert-demande-complet-by-type', perm='can_access',
name='insert-demande-complet-by-type',
post={ post={
'description': _('Insert demande complet by type'), 'description': _('Insert demande complet by type'),
'request_body': { 'request_body': {'schema': {'application/json': schemas.INSERT_DEMANDE_COMPLET_BY_TYPE}},
'schema': { },
'application/json': schemas.INSERT_DEMANDE_COMPLET_BY_TYPE
}
}
}
) )
def insert_demande_complet_by_type(self, request, post_data): def insert_demande_complet_by_type(self, request, post_data):
data = {} data = {}
@ -170,39 +162,39 @@ class ATALConnector(BaseResource):
if recv in post_data: if recv in post_data:
data[send] = post_data[recv] data[send] = post_data[recv]
demande_number = self._soap_call( demande_number = self._soap_call(wsdl='DemandeService', method='insertDemandeCompletByType', **data)
wsdl='DemandeService', method='insertDemandeCompletByType', **data
)
return process_response(demande_number) return process_response(demande_number)
@endpoint( @endpoint(
methods=['get'], perm='can_access', example_pattern='{demande_number}/', methods=['get'],
pattern='^(?P<demande_number>\w+)/$', name='retrieve-details-demande', perm='can_access',
parameters={ example_pattern='{demande_number}/',
'demande_number': DEMANDE_NUMBER_PARAM pattern='^(?P<demande_number>\w+)/$',
} name='retrieve-details-demande',
parameters={'demande_number': DEMANDE_NUMBER_PARAM},
) )
def retrieve_details_demande(self, request, demande_number): def retrieve_details_demande(self, request, demande_number):
soap_res = self._soap_call( soap_res = self._soap_call(
wsdl='DemandeService', method='retrieveDetailsDemande', wsdl='DemandeService', method='retrieveDetailsDemande', demandeNumberParam=demande_number
demandeNumberParam=demande_number) )
return {'data': helpers.serialize_object(soap_res)} return {'data': helpers.serialize_object(soap_res)}
@endpoint( @endpoint(
methods=['get'], perm='can_access', example_pattern='{demande_number}/', methods=['get'],
pattern='^(?P<demande_number>\w+)/$', name='retrieve-etat-travaux', perm='can_access',
parameters={ example_pattern='{demande_number}/',
'demande_number': DEMANDE_NUMBER_PARAM pattern='^(?P<demande_number>\w+)/$',
} name='retrieve-etat-travaux',
parameters={'demande_number': DEMANDE_NUMBER_PARAM},
) )
def retrieve_etat_travaux(self, request, demande_number): def retrieve_etat_travaux(self, request, demande_number):
soap_res = self._soap_call( soap_res = self._soap_call(wsdl='DemandeService', method='retrieveEtatTravaux', numero=demande_number)
wsdl='DemandeService', method='retrieveEtatTravaux',
numero=demande_number)
return {'data': helpers.serialize_object(soap_res)} return {'data': helpers.serialize_object(soap_res)}
@endpoint( @endpoint(
methods=['get'], perm='can_access', example_pattern='{demande_number}/', methods=['get'],
perm='can_access',
example_pattern='{demande_number}/',
pattern='^(?P<demande_number>\w+)/$', pattern='^(?P<demande_number>\w+)/$',
parameters={ parameters={
'demande_number': DEMANDE_NUMBER_PARAM, 'demande_number': DEMANDE_NUMBER_PARAM,
@ -210,14 +202,14 @@ class ATALConnector(BaseResource):
'description': _('Full'), 'description': _('Full'),
'example_value': 'true', 'example_value': 'true',
'type': 'bool', 'type': 'bool',
} },
} },
) )
def infos(self, request, demande_number, full=False): def infos(self, request, demande_number, full=False):
demand_details = helpers.serialize_object( demand_details = helpers.serialize_object(
self._soap_call( self._soap_call(
wsdl='DemandeService', method='retrieveDetailsDemande', wsdl='DemandeService', method='retrieveDetailsDemande', demandeNumberParam=demande_number
demandeNumberParam=demande_number) )
) )
if not demand_details: if not demand_details:
raise APIError('Could not get a status') raise APIError('Could not get a status')
@ -230,18 +222,12 @@ class ATALConnector(BaseResource):
works_comments = [] works_comments = []
if responses: if responses:
for response in responses: for response in responses:
comment = { comment = {'text': response.get('commentaires'), 'date': None}
'text': response.get('commentaires'),
'date': None
}
if 'dateReponse' in response: if 'dateReponse' in response:
comment['date'] = dateformat.format(response['dateReponse'], DATE_FORMAT) comment['date'] = dateformat.format(response['dateReponse'], DATE_FORMAT)
works_comments.append(comment) works_comments.append(comment)
works_comment = { works_comment = {'text': None, 'date': None}
'text': None,
'date': None
}
if works_comments: if works_comments:
works_comment = works_comments[-1] works_comment = works_comments[-1]
@ -249,22 +235,17 @@ class ATALConnector(BaseResource):
'status': status, 'status': status,
'works_comment': works_comment, 'works_comment': works_comment,
'demand_details': None, 'demand_details': None,
'works_comments': [] 'works_comments': [],
} }
if full: if full:
data['demand_details'] = demand_details data['demand_details'] = demand_details
data['works_comments'] = works_comments data['works_comments'] = works_comments
if status not in ('PRISE EN COMPTE', 'ARCHIVEE'): if status not in ('PRISE EN COMPTE', 'ARCHIVEE'):
return { return {'data': data}
'data': data
}
works_status = helpers.serialize_object( works_status = helpers.serialize_object(
self._soap_call( self._soap_call(wsdl='DemandeService', method='retrieveEtatTravaux', numero=demande_number)
wsdl='DemandeService', method='retrieveEtatTravaux',
numero=demande_number
)
) )
status = works_status.get('libelle') status = works_status.get('libelle')
if not status: if not status:
@ -277,20 +258,14 @@ class ATALConnector(BaseResource):
if full: if full:
data['works_status'] = works_status data['works_status'] = works_status
return { return {'data': data}
'data': data
}
@endpoint( @endpoint(
perm='can_access', perm='can_access',
post={ post={
'description': _('Upload a file'), 'description': _('Upload a file'),
'request_body': { 'request_body': {'schema': {'application/json': schemas.UPLOAD}},
'schema': { },
'application/json': schemas.UPLOAD
}
}
}
) )
def upload(self, request, post_data): def upload(self, request, post_data):
try: try:
@ -301,23 +276,22 @@ class ATALConnector(BaseResource):
data = { data = {
'donneesFichier': content, 'donneesFichier': content,
'numeroDemande': post_data['numero_demande'], 'numeroDemande': post_data['numero_demande'],
'nomFichier': post_data['nom_fichier'] 'nomFichier': post_data['nom_fichier'],
} }
self._soap_call( self._soap_call(wsdl='ChargementPiecesJointesService', method='upload', **data)
wsdl='ChargementPiecesJointesService', method='upload',
**data
)
return {} return {}
@endpoint( @endpoint(
methods=['get'], perm='can_access', example_pattern='{demande_number}/', methods=['get'],
pattern='^(?P<demande_number>\w+)/$', name='new-comments', perm='can_access',
example_pattern='{demande_number}/',
pattern='^(?P<demande_number>\w+)/$',
name='new-comments',
parameters={ parameters={
'demande_number': DEMANDE_NUMBER_PARAM, 'demande_number': DEMANDE_NUMBER_PARAM,
} },
) )
def new_comments(self, request, demande_number, last_datetime=None): def new_comments(self, request, demande_number, last_datetime=None):
def issup(datetime1, datetime2): def issup(datetime1, datetime2):
if datetime1.tzinfo is None or datetime2.tzinfo is None: if datetime1.tzinfo is None or datetime2.tzinfo is None:
datetime1 = datetime1.replace(tzinfo=None) datetime1 = datetime1.replace(tzinfo=None)
@ -331,8 +305,8 @@ class ATALConnector(BaseResource):
demand_details = helpers.serialize_object( demand_details = helpers.serialize_object(
self._soap_call( self._soap_call(
wsdl='DemandeService', method='retrieveDetailsDemande', wsdl='DemandeService', method='retrieveDetailsDemande', demandeNumberParam=demande_number
demandeNumberParam=demande_number) )
) )
if not demand_details: if not demand_details:
raise APIError('Could not get comments') raise APIError('Could not get comments')
@ -340,11 +314,7 @@ class ATALConnector(BaseResource):
new_comments, all_comments, last_date = [], [], None new_comments, all_comments, last_date = [], [], None
responses = (demand_details.get('reponses') or {}).get('Reponse') or [] responses = (demand_details.get('reponses') or {}).get('Reponse') or []
for response in responses: for response in responses:
comment = { comment = {'text': response.get('commentaires'), 'date': None, 'date_raw': None}
'text': response.get('commentaires'),
'date': None,
'date_raw': None
}
dateobj = None dateobj = None
if 'dateReponse' in response: if 'dateReponse' in response:
dateobj = response['dateReponse'] dateobj = response['dateReponse']
@ -356,10 +326,4 @@ class ATALConnector(BaseResource):
if dateobj and issup(dateobj, last_datetime) or last_datetime is None: if dateobj and issup(dateobj, last_datetime) or last_datetime is None:
if comment not in new_comments: if comment not in new_comments:
new_comments.append(comment) new_comments.append(comment)
return { return {'data': {'new_comments': new_comments, 'all_comments': all_comments, 'last_date': last_date}}
'data': {
'new_comments': new_comments,
'all_comments': all_comments,
'last_date': last_date
}
}

View File

@ -122,25 +122,15 @@ INSERT_DEMANDE_COMPLET_BY_TYPE = {
'demande_commentaire': { 'demande_commentaire': {
'type': 'string', 'type': 'string',
}, },
'remote_adresse': { 'remote_adresse': {'type': 'string'},
'type': 'string' 'demande_mots_cles': {'type': 'string'},
},
'demande_mots_cles': {
'type': 'string'
},
'code_thematique': { 'code_thematique': {
'type': 'string', 'type': 'string',
}, },
'code_priorite': { 'code_priorite': {'type': 'string'},
'type': 'string' 'demande_thematique': {'type': 'string'},
}, 'code_projet': {'type': 'string'},
'demande_thematique': { },
'type': 'string'
},
'code_projet': {
'type': 'string'
}
}
} }
INSERT_ACTION_COMMENT = { INSERT_ACTION_COMMENT = {
@ -153,8 +143,8 @@ INSERT_ACTION_COMMENT = {
}, },
'commentaire': { 'commentaire': {
'type': 'string', 'type': 'string',
} },
} },
} }
UPLOAD = { UPLOAD = {
@ -169,13 +159,13 @@ UPLOAD = {
'content': { 'content': {
'type': 'string', 'type': 'string',
}, },
} },
}, },
'numero_demande': { 'numero_demande': {
'type': 'string', 'type': 'string',
}, },
'nom_fichier': { 'nom_fichier': {
'type': 'string', 'type': 'string',
} },
} },
} }

View File

@ -19,7 +19,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Link', name='Link',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name_id', models.CharField(max_length=256, verbose_name='NameID')), ('name_id', models.CharField(max_length=256, verbose_name='NameID')),
('id_per', models.CharField(max_length=64, verbose_name='ID Per')), ('id_per', models.CharField(max_length=64, verbose_name='ID Per')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')),
@ -32,20 +35,65 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Resource', name='Resource',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('log_level', models.CharField(choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL'), (b'FATAL', b'FATAL')], default=b'INFO', max_length=10, verbose_name='Log Level')), (
('basic_auth_username', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication username')), 'log_level',
('basic_auth_password', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication password')), models.CharField(
('client_certificate', models.FileField(blank=True, null=True, upload_to=b'', verbose_name='TLS client certificate')), choices=[
('trusted_certificate_authorities', models.FileField(blank=True, null=True, upload_to=b'', verbose_name='TLS trusted CAs')), (b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
(b'FATAL', b'FATAL'),
],
default=b'INFO',
max_length=10,
verbose_name='Log Level',
),
),
(
'basic_auth_username',
models.CharField(
blank=True, max_length=128, verbose_name='Basic authentication username'
),
),
(
'basic_auth_password',
models.CharField(
blank=True, max_length=128, verbose_name='Basic authentication password'
),
),
(
'client_certificate',
models.FileField(
blank=True, null=True, upload_to=b'', verbose_name='TLS client certificate'
),
),
(
'trusted_certificate_authorities',
models.FileField(blank=True, null=True, upload_to=b'', verbose_name='TLS trusted CAs'),
),
('verify_cert', models.BooleanField(default=True, verbose_name='TLS verify certificates')), ('verify_cert', models.BooleanField(default=True, verbose_name='TLS verify certificates')),
('http_proxy', models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy')), (
'http_proxy',
models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'),
),
('webservice_base_url', models.URLField(verbose_name='Webservice Base URL')), ('webservice_base_url', models.URLField(verbose_name='Webservice Base URL')),
('cod_rgp', models.CharField(default=b'RGP_PUB', max_length=64, verbose_name='Code RGP')), ('cod_rgp', models.CharField(default=b'RGP_PUB', max_length=64, verbose_name='Code RGP')),
('users', models.ManyToManyField(blank=True, related_name='_link_users_+', related_query_name='+', to='base.ApiUser')), (
'users',
models.ManyToManyField(
blank=True, related_name='_link_users_+', related_query_name='+', to='base.ApiUser'
),
),
], ],
options={ options={
'verbose_name': 'ATOS Genesys', 'verbose_name': 'ATOS Genesys',

View File

@ -15,7 +15,9 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='resource', model_name='resource',
name='client_certificate', name='client_certificate',
field=models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS client certificate'), field=models.FileField(
blank=True, null=True, upload_to='', verbose_name='TLS client certificate'
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='resource', model_name='resource',

View File

@ -93,7 +93,7 @@ class Resource(BaseResource, HTTPResource):
continue continue
categories[xmlutils.text_content(code)] = { categories[xmlutils.text_content(code)] = {
'label': xmlutils.text_content(label), 'label': xmlutils.text_content(label),
'codifications': [] 'codifications': [],
} }
for codification in root.findall('CODIFICATIONS/CODIFICATIONS_ROW'): for codification in root.findall('CODIFICATIONS/CODIFICATIONS_ROW'):
code = codification.find('CD_CODIF') code = codification.find('CD_CODIF')
@ -107,11 +107,15 @@ class Resource(BaseResource, HTTPResource):
if category_cod not in categories: if category_cod not in categories:
self.logger.warning('unknown category: %s', category_cod) self.logger.warning('unknown category: %s', category_cod)
continue continue
categories[category_cod]['codifications'].append({ categories[category_cod]['codifications'].append(
'code': xmlutils.text_content(code), {
'label': xmlutils.text_content(label), 'code': xmlutils.text_content(code),
'enabled': xmlutils.text_content(in_val).strip().lower() == 'o' if in_val is not None else True, 'label': xmlutils.text_content(label),
}) 'enabled': xmlutils.text_content(in_val).strip().lower() == 'o'
if in_val is not None
else True,
}
)
return categories return categories
def get_codifications(self): def get_codifications(self):
@ -119,40 +123,43 @@ class Resource(BaseResource, HTTPResource):
function=self.call_select_codifications, function=self.call_select_codifications,
row=self, row=self,
key_prefix='atos-genesys-codifications', key_prefix='atos-genesys-codifications',
logger=self.logger) logger=self.logger,
)
return cache() return cache()
@endpoint(name='codifications', @endpoint(name='codifications', description=_('List of codifications categories'))
description=_('List of codifications categories'))
def codifications(self, request): def codifications(self, request):
codifications = self.get_codifications() codifications = self.get_codifications()
items = [] items = []
for code, category in codifications.items(): for code, category in codifications.items():
items.append({ items.append(
'id': code, {
'label': category['label'], 'id': code,
}) 'label': category['label'],
}
)
items.sort(key=lambda c: c['label']) items.sort(key=lambda c: c['label'])
return {'data': items} return {'data': items}
@endpoint(name='codifications', @endpoint(
pattern=r'^(?P<category>[\w-]+)/$', name='codifications',
example_pattern='{category}/', pattern=r'^(?P<category>[\w-]+)/$',
description=_('List of codifications'), example_pattern='{category}/',
parameters={ description=_('List of codifications'),
'category': { parameters={
'description': _('Category of codification'), 'category': {
'example_value': u'MOT_APA', 'description': _('Category of codification'),
} 'example_value': u'MOT_APA',
}) }
},
)
def codifications_list(self, request, category): def codifications_list(self, request, category):
codifications = self.get_codifications().get(category, {}).get('codifications', []) codifications = self.get_codifications().get(category, {}).get('codifications', [])
items = [{ items = [
'id': codification['code'], {'id': codification['code'], 'text': codification['label']} for codification in codifications
'text': codification['label'] ]
} for codification in codifications]
return {'data': items} return {'data': items}
def check_status(self): def check_status(self):
@ -163,11 +170,14 @@ class Resource(BaseResource, HTTPResource):
return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectAppairage') return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectAppairage')
def call_select_appairage(self, login, password, email): def call_select_appairage(self, login, password, email):
row = self.xml_request(self.select_appairage_url, params={ row = self.xml_request(
'login': login, self.select_appairage_url,
'pwd': password, params={
'email': email, 'login': login,
}) 'pwd': password,
'email': email,
},
)
row_d = xmlutils.to_json(row) row_d = xmlutils.to_json(row)
id_per = row_d.get('ID_PER', '').strip() id_per = row_d.get('ID_PER', '').strip()
code = row_d.get('CD_RET', '').strip() code = row_d.get('CD_RET', '').strip()
@ -175,72 +185,70 @@ class Resource(BaseResource, HTTPResource):
error = None error = None
if code not in ['1', '2', '3', '4', '5', '6']: if code not in ['1', '2', '3', '4', '5', '6']:
error = 'invalid CD_RET: %s' % code, error = ('invalid CD_RET: %s' % code,)
if code in ['2', '3', '5'] and not id_per: if code in ['2', '3', '5'] and not id_per:
error = 'missing ID_PER' error = 'missing ID_PER'
if error: if error:
raise APIError(error, data={'response': repr(ET.tostring(row))}) raise APIError(error, data={'response': repr(ET.tostring(row))})
return code, label, id_per return code, label, id_per
@endpoint(name='link', @endpoint(
methods=['post'], name='link',
description=_('Create link with an extranet account'), methods=['post'],
perm='can_access', description=_('Create link with an extranet account'),
parameters={ perm='can_access',
'NameID':{ parameters={
'description': _('Publik NameID'), 'NameID': {
'example_value': 'xyz24d934', 'description': _('Publik NameID'),
}, 'example_value': 'xyz24d934',
'email': { },
'description': _('Publik known email'), 'email': {
'example_value': 'john.doe@example.com', 'description': _('Publik known email'),
}, 'example_value': 'john.doe@example.com',
'login': { },
'description': _('ATOS Genesys extranet login'), 'login': {
'example_value': '1234', 'description': _('ATOS Genesys extranet login'),
}, 'example_value': '1234',
'password': { },
'description': _('ATOS Genesys extranet password'), 'password': {
'example_value': 'password', 'description': _('ATOS Genesys extranet password'),
} 'example_value': 'password',
}) },
},
)
def link(self, request, NameID, email, login, password): def link(self, request, NameID, email, login, password):
code, label, id_per = self.call_select_appairage(login, password, email) code, label, id_per = self.call_select_appairage(login, password, email)
if code in ['2', '3', '5']: if code in ['2', '3', '5']:
link, created = Link.objects.get_or_create( link, created = Link.objects.get_or_create(resource=self, name_id=NameID, id_per=id_per)
resource=self,
name_id=NameID,
id_per=id_per)
return {'link_id': link.pk, 'new': created, 'code': code, 'label': label} return {'link_id': link.pk, 'new': created, 'code': code, 'label': label}
elif code == '6': elif code == '6':
raise APIError('unknown-login', data={'code': code, 'label': label}) raise APIError('unknown-login', data={'code': code, 'label': label})
elif code in ['4', '1']: elif code in ['4', '1']:
raise APIError('invalid-password', data={'code': code, 'label': label}) raise APIError('invalid-password', data={'code': code, 'label': label})
@endpoint(name='unlink', @endpoint(
methods=['post'], name='unlink',
description=_('Delete link with an extranet account'), methods=['post'],
perm='can_access', description=_('Delete link with an extranet account'),
parameters={ perm='can_access',
'NameID':{ parameters={
'description': _('Publik NameID'), 'NameID': {
'example_value': 'xyz24d934', 'description': _('Publik NameID'),
}, 'example_value': 'xyz24d934',
'link_id': { },
'description': _('Identifier of the link'), 'link_id': {
'example_value': '1', 'description': _('Identifier of the link'),
}, 'example_value': '1',
}) },
},
)
def unlink(self, request, NameID, link_id): def unlink(self, request, NameID, link_id):
try: try:
link_id = int(link_id.strip()) link_id = int(link_id.strip())
except ValueError: except ValueError:
raise APIError('invalid link_id') raise APIError('invalid link_id')
qs = Link.objects.filter( qs = Link.objects.filter(resource=self, name_id=NameID, pk=link_id)
resource=self,
name_id=NameID,
pk=link_id)
count = qs.count() count = qs.count()
qs.delete() qs.delete()
return {'deleted': count} return {'deleted': count}
@ -250,10 +258,13 @@ class Resource(BaseResource, HTTPResource):
return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectUsager') return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectUsager')
def call_select_usager(self, id_per): def call_select_usager(self, id_per):
row = self.xml_request(self.select_usager_url, params={ row = self.xml_request(
'idPer': id_per, self.select_usager_url,
'codRgp': self.cod_rgp, params={
}) 'idPer': id_per,
'codRgp': self.cod_rgp,
},
)
return self._select_usager_row_to_json(row) return self._select_usager_row_to_json(row)
def _select_usager_row_to_json(self, row): def _select_usager_row_to_json(self, row):
@ -275,19 +286,19 @@ class Resource(BaseResource, HTTPResource):
identification['CIVILITE'] = {'M': u'Monsieur', 'F': u'Madame'}.get(sexe, '') identification['CIVILITE'] = {'M': u'Monsieur', 'F': u'Madame'}.get(sexe, '')
return d return d
@endpoint(name='dossiers', @endpoint(
description=_('Get datas for all links'), name='dossiers',
perm='can_access', description=_('Get datas for all links'),
parameters={ perm='can_access',
'NameID':{ parameters={
'description': _('Publik NameID'), 'NameID': {
'example_value': 'xyz24d934', 'description': _('Publik NameID'),
}, 'example_value': 'xyz24d934',
}) },
},
)
def dossiers(self, request, NameID, link_id=None): def dossiers(self, request, NameID, link_id=None):
qs = Link.objects.filter( qs = Link.objects.filter(resource=self, name_id=NameID)
resource=self,
name_id=NameID)
if link_id: if link_id:
try: try:
link_id = int(link_id) link_id = int(link_id)
@ -300,7 +311,8 @@ class Resource(BaseResource, HTTPResource):
function=self.call_select_usager, function=self.call_select_usager,
row=link, row=link,
key_prefix='atos-genesys-usager', key_prefix='atos-genesys-usager',
logger=self.logger) logger=self.logger,
)
dossier = cache(link.id_per) dossier = cache(link.id_per)
# build text as "id_per - prenom - no # build text as "id_per - prenom - no
text_parts = [str(link.id_per), '-'] text_parts = [str(link.id_per), '-']
@ -312,12 +324,14 @@ class Resource(BaseResource, HTTPResource):
text_parts.append(prenom.title()) text_parts.append(prenom.title())
if nom: if nom:
text_parts.append(nom.upper()) text_parts.append(nom.upper())
data.append({ data.append(
'id': str(link.id), {
'text': u' '.join(text_parts), 'id': str(link.id),
'id_per': link.id_per, 'text': u' '.join(text_parts),
'dossier': dossier, 'id_per': link.id_per,
}) 'dossier': dossier,
}
)
if link_id: if link_id:
return {'data': data[0] if data else None} return {'data': data[0] if data else None}
return {'data': data} return {'data': data}
@ -327,10 +341,13 @@ class Resource(BaseResource, HTTPResource):
return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectUsagerByRef') return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/selectUsagerByRef')
def call_select_usager_by_ref(self, ref_per): def call_select_usager_by_ref(self, ref_per):
row = self.xml_request(self.select_usager_by_ref_url, params={ row = self.xml_request(
'refPer': ref_per, self.select_usager_by_ref_url,
'codRgp': self.cod_rgp, params={
}) 'refPer': ref_per,
'codRgp': self.cod_rgp,
},
)
return self._select_usager_row_to_json(row) return self._select_usager_row_to_json(row)
@property @property
@ -338,31 +355,36 @@ class Resource(BaseResource, HTTPResource):
return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/chercheBeneficiaire') return urlparse.urljoin(self.base_url, 'WSUsagerPublik/services/PublikService/chercheBeneficiaire')
def call_cherche_beneficiaire(self, prenom, nom, dob): def call_cherche_beneficiaire(self, prenom, nom, dob):
rows = self.xml_request_multiple(self.cherche_beneficiaire_url, params={ rows = self.xml_request_multiple(
'nmPer': nom, self.cherche_beneficiaire_url,
'prPer': prenom, params={
'dtNaissance': dob.strftime('%d/%m/%Y'), 'nmPer': nom,
}) 'prPer': prenom,
'dtNaissance': dob.strftime('%d/%m/%Y'),
},
)
beneficiaires = [xmlutils.to_json(row) for row in rows] beneficiaires = [xmlutils.to_json(row) for row in rows]
return beneficiaires return beneficiaires
@endpoint(name='search', @endpoint(
description=_('Search for beneficiaries'), name='search',
perm='can_access', description=_('Search for beneficiaries'),
parameters={ perm='can_access',
'first_name': { parameters={
'description': _('Beneficiary first name'), 'first_name': {
'example_value': 'John', 'description': _('Beneficiary first name'),
}, 'example_value': 'John',
'last_name': { },
'description': _('Beneficiary last name'), 'last_name': {
'example_value': 'Doe', 'description': _('Beneficiary last name'),
}, 'example_value': 'Doe',
'date_of_birth': { },
'description': _('Beneficiary date of birth'), 'date_of_birth': {
'example_value': '1987-10-23', 'description': _('Beneficiary date of birth'),
} 'example_value': '1987-10-23',
}) },
},
)
def search(self, request, first_name, last_name, date_of_birth, NameID=None, commune_naissance=None): def search(self, request, first_name, last_name, date_of_birth, NameID=None, commune_naissance=None):
try: try:
date_of_birth = datetime.datetime.strptime(date_of_birth, '%Y-%m-%d').date() date_of_birth = datetime.datetime.strptime(date_of_birth, '%Y-%m-%d').date()
@ -373,10 +395,7 @@ class Resource(BaseResource, HTTPResource):
if commune_naissance: if commune_naissance:
# convert commune_naissance to ASCII # convert commune_naissance to ASCII
commune_naissance = to_ascii(commune_naissance).lower() commune_naissance = to_ascii(commune_naissance).lower()
beneficiaires = self.call_cherche_beneficiaire( beneficiaires = self.call_cherche_beneficiaire(prenom=first_name, nom=last_name, dob=date_of_birth)
prenom=first_name,
nom=last_name,
dob=date_of_birth)
data = [] data = []
dossiers = [] dossiers = []
# get dossiers of found beneficiaries # get dossiers of found beneficiaries
@ -410,8 +429,12 @@ class Resource(BaseResource, HTTPResource):
if commune_naissance: if commune_naissance:
cmu_nais = to_ascii(identification.get('CMU_NAIS', '')).lower() cmu_nais = to_ascii(identification.get('CMU_NAIS', '')).lower()
if cmu_nais and commune_naissance != cmu_nais: if cmu_nais and commune_naissance != cmu_nais:
self.logger.debug(u'id_per %s: CMU_NAIS(%s) does not match commune_naissance(%s)', self.logger.debug(
id_per, cmu_nais, commune_naissance) u'id_per %s: CMU_NAIS(%s) does not match commune_naissance(%s)',
id_per,
cmu_nais,
commune_naissance,
)
continue continue
dossiers.append(dossier) dossiers.append(dossier)
@ -431,38 +454,41 @@ class Resource(BaseResource, HTTPResource):
tel2 = ''.join(c for c in identification.get('TEL_FIXE', '') if is_number(c)) tel2 = ''.join(c for c in identification.get('TEL_FIXE', '') if is_number(c))
email = identification.get('MAIL', '').strip() email = identification.get('MAIL', '').strip()
if tel1 and tel1[:2] in ('06', '07'): if tel1 and tel1[:2] in ('06', '07'):
data.append({ data.append(
'id': 'tel1', {
'text': 'par SMS vers ' + tel1[:2] + '*****' + tel1[-3:], 'id': 'tel1',
'phone': tel1, 'text': 'par SMS vers ' + tel1[:2] + '*****' + tel1[-3:],
'phone': tel1,
'id_per': id_per, 'id_per': id_per,
'nom': nom, 'nom': nom,
'prenom': prenom, 'prenom': prenom,
'nom_naissance': nom_naissance, 'nom_naissance': nom_naissance,
}) }
)
if tel2 and tel2[:2] in ('06', '07'): if tel2 and tel2[:2] in ('06', '07'):
data.append({ data.append(
'id': 'tel2', {
'text': 'par SMS vers ' + tel2[:2] + '*****' + tel2[-3:], 'id': 'tel2',
'phone': tel2, 'text': 'par SMS vers ' + tel2[:2] + '*****' + tel2[-3:],
'phone': tel2,
'id_per': id_per, 'id_per': id_per,
'nom': nom, 'nom': nom,
'prenom': prenom, 'prenom': prenom,
'nom_naissance': nom_naissance, 'nom_naissance': nom_naissance,
}) }
)
if email: if email:
data.append({ data.append(
'id': 'email1', {
'text': 'par courriel vers ' + email[:2] + '***@***' + email[-3:], 'id': 'email1',
'email': email, 'text': 'par courriel vers ' + email[:2] + '***@***' + email[-3:],
'email': email,
'id_per': id_per, 'id_per': id_per,
'nom': nom, 'nom': nom,
'prenom': prenom, 'prenom': prenom,
'nom_naissance': nom_naissance, 'nom_naissance': nom_naissance,
}) }
)
if len(data) == 0: if len(data) == 0:
self.logger.debug('id_per %s: no contact information, ignored', id_per) self.logger.debug('id_per %s: no contact information, ignored', id_per)
raise APIError('no-contacts') raise APIError('no-contacts')
@ -476,50 +502,39 @@ class Resource(BaseResource, HTTPResource):
'link_id': link and link.id, 'link_id': link and link.id,
} }
@endpoint(name='link-by-id-per', @endpoint(
methods=['post'], name='link-by-id-per',
description=_('Create link with an extranet account'), methods=['post'],
perm='can_access', description=_('Create link with an extranet account'),
parameters={ perm='can_access',
'NameID': { parameters={
'description': _('Publik NameID'), 'NameID': {
'example_value': 'xyz24d934', 'description': _('Publik NameID'),
}, 'example_value': 'xyz24d934',
'id_per': { },
'description': _('ATOS Genesys ID_PER'), 'id_per': {
'example_value': '767676', 'description': _('ATOS Genesys ID_PER'),
} 'example_value': '767676',
}) },
},
)
def link_by_id_per(self, request, NameID, id_per): def link_by_id_per(self, request, NameID, id_per):
dossier = self.call_select_usager(id_per) dossier = self.call_select_usager(id_per)
link, created = Link.objects.get_or_create( link, created = Link.objects.get_or_create(resource=self, name_id=NameID, id_per=id_per)
resource=self,
name_id=NameID,
id_per=id_per)
return {'link_id': link.pk, 'new': created} return {'link_id': link.pk, 'new': created}
class Link(models.Model): class Link(models.Model):
resource = models.ForeignKey( resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
Resource, name_id = models.CharField(verbose_name=_('NameID'), blank=False, max_length=256)
on_delete=models.CASCADE) id_per = models.CharField(verbose_name=_('ID Per'), blank=False, max_length=64)
name_id = models.CharField( created = models.DateTimeField(verbose_name=_('Creation date'), auto_now_add=True)
verbose_name=_('NameID'), extra = JSONField(verbose_name=_('Anything'), null=True)
blank=False,
max_length=256)
id_per = models.CharField(
verbose_name=_('ID Per'),
blank=False,
max_length=64)
created = models.DateTimeField(
verbose_name=_('Creation date'),
auto_now_add=True)
extra = JSONField(
verbose_name=_('Anything'),
null=True)
class Meta: class Meta:
unique_together = ( unique_together = (
'resource', 'name_id', 'id_per', 'resource',
'name_id',
'id_per',
) )
ordering = ['created'] ordering = ['created']

View File

@ -25,11 +25,12 @@ def row_lock(row):
class RowLockedCache(object): class RowLockedCache(object):
'''Cache return value of a function, always return the cached value for """Cache return value of a function, always return the cached value for
performance but if the cache is stale update it asynchronously using performance but if the cache is stale update it asynchronously using
a thread, prevent multiple update using row locks on database models and a thread, prevent multiple update using row locks on database models and
an update cache key. an update cache key.
''' """
def __init__(self, function, logger=None, row=None, duration=DEFAULT_DURATION, key_prefix=None): def __init__(self, function, logger=None, row=None, duration=DEFAULT_DURATION, key_prefix=None):
self.function = function self.function = function
self.row = row self.row = row

View File

@ -14,12 +14,28 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='BaseAddresse', name='BaseAddresse',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('service_url', models.CharField(help_text='Base Adresse Web Service URL', max_length=128, verbose_name='Service URL')), (
('users', models.ManyToManyField(to='base.ApiUser', related_name='_baseaddresse_users_+', related_query_name='+', blank=True)), 'service_url',
models.CharField(
help_text='Base Adresse Web Service URL', max_length=128, verbose_name='Service URL'
),
),
(
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_baseaddresse_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'verbose_name': 'Base Adresse Web Service', 'verbose_name': 'Base Adresse Web Service',

View File

@ -14,7 +14,12 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='baseaddresse', model_name='baseaddresse',
name='service_url', name='service_url',
field=models.CharField(default=b'https://api-adresse.data.gouv.fr/', help_text='Base Adresse Web Service URL', max_length=128, verbose_name='Service URL'), field=models.CharField(
default=b'https://api-adresse.data.gouv.fr/',
help_text='Base Adresse Web Service URL',
max_length=128,
verbose_name='Service URL',
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,7 +14,13 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='baseaddresse', model_name='baseaddresse',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Debug Enabled', blank=True, choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Debug Enabled',
blank=True,
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,7 +14,12 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='baseaddresse', model_name='baseaddresse',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,7 +14,19 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='baseaddresse', model_name='baseaddresse',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -10,6 +10,4 @@ class Migration(migrations.Migration):
('base_adresse', '0005_auto_20160407_0456'), ('base_adresse', '0005_auto_20160407_0456'),
] ]
operations = [ operations = [migrations.RenameModel('BaseAddresse', 'BaseAdresse')]
migrations.RenameModel('BaseAddresse', 'BaseAdresse')
]

View File

@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='StreetModel', name='StreetModel',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('city', models.CharField(max_length=100, verbose_name='City')), ('city', models.CharField(max_length=100, verbose_name='City')),
('name', models.CharField(max_length=150, verbose_name='Street name')), ('name', models.CharField(max_length=150, verbose_name='Street name')),
('zipcode', models.CharField(max_length=5, verbose_name='Postal code')), ('zipcode', models.CharField(max_length=5, verbose_name='Postal code')),
@ -26,7 +29,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='UpdateStreetModel', name='UpdateStreetModel',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('zipcode', models.CharField(max_length=5, verbose_name='Postal code')), ('zipcode', models.CharField(max_length=5, verbose_name='Postal code')),
('start_time', models.DateTimeField(null=True, verbose_name='Start of update')), ('start_time', models.DateTimeField(null=True, verbose_name='Start of update')),
('end_time', models.DateTimeField(null=True, verbose_name='End of update')), ('end_time', models.DateTimeField(null=True, verbose_name='End of update')),
@ -35,6 +41,8 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='baseadresse', model_name='baseadresse',
name='zipcode', name='zipcode',
field=models.CharField(max_length=5, verbose_name='Postal codes to get streets, separated with commas', blank=True), field=models.CharField(
max_length=5, verbose_name='Postal codes to get streets, separated with commas', blank=True
),
), ),
] ]

View File

@ -15,6 +15,10 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='baseadresse', model_name='baseadresse',
name='zipcode', name='zipcode',
field=models.CharField(blank=True, max_length=600, verbose_name='Postal codes or county number to get streets, separated with commas'), field=models.CharField(
blank=True,
max_length=600,
verbose_name='Postal codes or county number to get streets, separated with commas',
),
), ),
] ]

View File

@ -17,9 +17,15 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CityModel', name='CityModel',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name', models.CharField(max_length=150, verbose_name='City name')), ('name', models.CharField(max_length=150, verbose_name='City name')),
('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='City name ascii char')), (
'unaccent_name',
models.CharField(max_length=150, null=True, verbose_name='City name ascii char'),
),
('code', models.CharField(max_length=5, verbose_name='INSEE code')), ('code', models.CharField(max_length=5, verbose_name='INSEE code')),
('zipcode', models.CharField(max_length=5, verbose_name='Postal code')), ('zipcode', models.CharField(max_length=5, verbose_name='Postal code')),
('population', models.PositiveIntegerField(verbose_name='Population')), ('population', models.PositiveIntegerField(verbose_name='Population')),
@ -33,9 +39,15 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='DepartmentModel', name='DepartmentModel',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name', models.CharField(max_length=100, verbose_name='Department name')), ('name', models.CharField(max_length=100, verbose_name='Department name')),
('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='Department name ascii char')), (
'unaccent_name',
models.CharField(max_length=150, null=True, verbose_name='Department name ascii char'),
),
('code', models.CharField(max_length=3, unique=True, verbose_name='Department code')), ('code', models.CharField(max_length=3, unique=True, verbose_name='Department code')),
('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='Last update')), ('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='Last update')),
], ],
@ -47,9 +59,15 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='RegionModel', name='RegionModel',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name', models.CharField(max_length=150, verbose_name='Region name')), ('name', models.CharField(max_length=150, verbose_name='Region name')),
('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='Region name ascii char')), (
'unaccent_name',
models.CharField(max_length=150, null=True, verbose_name='Region name ascii char'),
),
('code', models.CharField(max_length=2, unique=True, verbose_name='Region code')), ('code', models.CharField(max_length=2, unique=True, verbose_name='Region code')),
('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='Last update')), ('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='Last update')),
], ],
@ -61,12 +79,21 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='baseadresse', model_name='baseadresse',
name='api_geo_url', name='api_geo_url',
field=models.CharField(default=b'https://geo.api.gouv.fr/', help_text='Base Adresse API Geo URL', max_length=128, verbose_name='API Geo URL'), field=models.CharField(
default=b'https://geo.api.gouv.fr/',
help_text='Base Adresse API Geo URL',
max_length=128,
verbose_name='API Geo URL',
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='baseadresse', model_name='baseadresse',
name='zipcode', name='zipcode',
field=models.CharField(blank=True, max_length=600, verbose_name='Postal codes or department number to get streets, separated with commas'), field=models.CharField(
blank=True,
max_length=600,
verbose_name='Postal codes or department number to get streets, separated with commas',
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='streetmodel', model_name='streetmodel',
@ -76,17 +103,29 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='departmentmodel', model_name='departmentmodel',
name='region', name='region',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base_adresse.RegionModel'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to='base_adresse.RegionModel'
),
), ),
migrations.AddField( migrations.AddField(
model_name='citymodel', model_name='citymodel',
name='department', name='department',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base_adresse.DepartmentModel'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='base_adresse.DepartmentModel',
),
), ),
migrations.AddField( migrations.AddField(
model_name='citymodel', model_name='citymodel',
name='region', name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base_adresse.RegionModel'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='base_adresse.RegionModel',
),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='citymodel', name='citymodel',

View File

@ -16,7 +16,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='AddressCacheModel', name='AddressCacheModel',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('api_id', models.CharField(max_length=30, unique=True)), ('api_id', models.CharField(max_length=30, unique=True)),
('data', django.contrib.postgres.fields.jsonb.JSONField(default=dict)), ('data', django.contrib.postgres.fields.jsonb.JSONField(default=dict)),
('timestamp', models.DateTimeField(auto_now=True)), ('timestamp', models.DateTimeField(auto_now=True)),
@ -25,11 +28,21 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='baseadresse', model_name='baseadresse',
name='latitude', name='latitude',
field=models.FloatField(blank=True, help_text='Geographic priority for /addresses/ endpoint.', null=True, verbose_name='Latitude'), field=models.FloatField(
blank=True,
help_text='Geographic priority for /addresses/ endpoint.',
null=True,
verbose_name='Latitude',
),
), ),
migrations.AddField( migrations.AddField(
model_name='baseadresse', model_name='baseadresse',
name='longitude', name='longitude',
field=models.FloatField(blank=True, help_text='Geographic priority for /addresses/ endpoint.', null=True, verbose_name='Longitude'), field=models.FloatField(
blank=True,
help_text='Geographic priority for /addresses/ endpoint.',
null=True,
verbose_name='Longitude',
),
), ),
] ]

View File

@ -21,11 +21,21 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='baseadresse', model_name='baseadresse',
name='api_geo_url', name='api_geo_url',
field=models.CharField(default='https://geo.api.gouv.fr/', help_text='Base Adresse API Geo URL', max_length=128, verbose_name='API Geo URL'), field=models.CharField(
default='https://geo.api.gouv.fr/',
help_text='Base Adresse API Geo URL',
max_length=128,
verbose_name='API Geo URL',
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='baseadresse', model_name='baseadresse',
name='service_url', name='service_url',
field=models.CharField(default='https://api-adresse.data.gouv.fr/', help_text='Base Adresse Web Service URL', max_length=128, verbose_name='Service URL'), field=models.CharField(
default='https://api-adresse.data.gouv.fr/',
help_text='Base Adresse Web Service URL',
max_length=128,
verbose_name='Service URL',
),
), ),
] ]

View File

@ -22,16 +22,20 @@ from passerelle.utils.jsonresponse import APIError
class BaseAdresse(BaseResource): class BaseAdresse(BaseResource):
service_url = models.CharField( service_url = models.CharField(
max_length=128, blank=False, max_length=128,
blank=False,
default='https://api-adresse.data.gouv.fr/', default='https://api-adresse.data.gouv.fr/',
verbose_name=_('Service URL'), verbose_name=_('Service URL'),
help_text=_('Base Adresse Web Service URL')) help_text=_('Base Adresse Web Service URL'),
)
api_geo_url = models.CharField( api_geo_url = models.CharField(
max_length=128, blank=False, max_length=128,
blank=False,
default='https://geo.api.gouv.fr/', default='https://geo.api.gouv.fr/',
verbose_name=_('API Geo URL'), verbose_name=_('API Geo URL'),
help_text=_('Base Adresse API Geo URL')) help_text=_('Base Adresse API Geo URL'),
)
category = _('Geographic information system') category = _('Geographic information system')
@ -46,15 +50,18 @@ class BaseAdresse(BaseResource):
zipcode = models.CharField( zipcode = models.CharField(
max_length=600, max_length=600,
blank=True, blank=True,
verbose_name=_('Postal codes or department number to get streets, separated with commas')) verbose_name=_('Postal codes or department number to get streets, separated with commas'),
)
latitude = models.FloatField( latitude = models.FloatField(
null=True, blank=True, null=True,
blank=True,
verbose_name=_('Latitude'), verbose_name=_('Latitude'),
help_text=_('Geographic priority for /addresses/ endpoint.'), help_text=_('Geographic priority for /addresses/ endpoint.'),
) )
longitude = models.FloatField( longitude = models.FloatField(
null=True, blank=True, null=True,
blank=True,
verbose_name=_('Longitude'), verbose_name=_('Longitude'),
help_text=_('Geographic priority for /addresses/ endpoint.'), help_text=_('Geographic priority for /addresses/ endpoint.'),
) )
@ -78,29 +85,38 @@ class BaseAdresse(BaseResource):
elif prop == 'name': elif prop == 'name':
house_number = data['properties'].get('housenumber') house_number = data['properties'].get('housenumber')
if house_number and value.startswith(house_number): if house_number and value.startswith(house_number):
value = value[len(house_number):].strip() value = value[len(house_number) :].strip()
result['address']['road'] = value result['address']['road'] = value
elif prop == 'id': elif prop == 'id':
result['id'] = value result['id'] = value
return result return result
@endpoint(pattern='(?P<q>.+)?$', @endpoint(
description=_('Addresses list'), pattern='(?P<q>.+)?$',
parameters={ description=_('Addresses list'),
'id': {'description': _('Address identifier')}, parameters={
'q': {'description': _('Address'), 'example_value': '169 rue du chateau, paris'}, 'id': {'description': _('Address identifier')},
'page_limit': {'description': _('Maximum number of results to return. Must be ' 'q': {'description': _('Address'), 'example_value': '169 rue du chateau, paris'},
'lower than 20.')}, 'page_limit': {
'zipcode': {'description': _('Zipcode'), 'example_value': '75014'}, 'description': _('Maximum number of results to return. Must be ' 'lower than 20.')
'citycode': {'description': _('INSEE City code')}, },
'lat': {'description': _('Prioritize results according to coordinates. "lon" ' 'zipcode': {'description': _('Zipcode'), 'example_value': '75014'},
'parameter must also be present.')}, 'citycode': {'description': _('INSEE City code')},
'lon': {'description': _('Prioritize results according to coordinates. "lat" ' 'lat': {
'parameter must also be present.')}, 'description': _(
}) 'Prioritize results according to coordinates. "lon" ' 'parameter must also be present.'
def addresses(self, request, id=None, q=None, )
zipcode='', citycode=None, },
lat=None, lon=None, page_limit=5): 'lon': {
'description': _(
'Prioritize results according to coordinates. "lat" ' 'parameter must also be present.'
)
},
},
)
def addresses(
self, request, id=None, q=None, zipcode='', citycode=None, lat=None, lon=None, page_limit=5
):
if id is not None: if id is not None:
try: try:
address = AddressCacheModel.objects.get(api_id=id) address = AddressCacheModel.objects.get(api_id=id)
@ -145,34 +161,47 @@ class BaseAdresse(BaseResource):
data = self.format_address_data(feature) data = self.format_address_data(feature)
result.append(data) result.append(data)
address, created = AddressCacheModel.objects.get_or_create( address, created = AddressCacheModel.objects.get_or_create(
api_id=data['id'], defaults={'data': data}) api_id=data['id'], defaults={'data': data}
)
if not created: if not created:
address.update_timestamp() address.update_timestamp()
return {'data': result} return {'data': result}
@endpoint(pattern='(?P<q>.+)?$', description=_('Geocoding (Nominatim API)'), @endpoint(
parameters={ pattern='(?P<q>.+)?$',
'q': {'description': _('Address'), 'example_value': '169 rue du chateau, paris'}, description=_('Geocoding (Nominatim API)'),
'zipcode': {'description': _('Zipcode')}, parameters={
'citycode': {'description': _('INSEE City code')}, 'q': {'description': _('Address'), 'example_value': '169 rue du chateau, paris'},
'lat': {'description': _('Prioritize results according to coordinates. "lat" ' 'zipcode': {'description': _('Zipcode')},
'parameter must be present.')}, 'citycode': {'description': _('INSEE City code')},
'lon': {'description': _('Prioritize results according to coordinates. "lon" ' 'lat': {
'parameter must be present.')}, 'description': _(
}) 'Prioritize results according to coordinates. "lat" ' 'parameter must be present.'
)
},
'lon': {
'description': _(
'Prioritize results according to coordinates. "lon" ' 'parameter must be present.'
)
},
},
)
def search(self, request, q, zipcode='', citycode=None, lat=None, lon=None, **kwargs): def search(self, request, q, zipcode='', citycode=None, lat=None, lon=None, **kwargs):
if kwargs.get('format', 'json') != 'json': if kwargs.get('format', 'json') != 'json':
raise NotImplementedError() raise NotImplementedError()
result = self.addresses(request, q=q, zipcode=zipcode, citycode=citycode, result = self.addresses(
lat=lat, lon=lon, page_limit=1) request, q=q, zipcode=zipcode, citycode=citycode, lat=lat, lon=lon, page_limit=1
)
return result['data'] return result['data']
@endpoint(description=_('Reverse geocoding'), @endpoint(
parameters={ description=_('Reverse geocoding'),
'lat': {'description': _('Latitude'), 'example_value': 48.833708}, parameters={
'lon': {'description': _('Longitude'), 'example_value': 2.323349}, 'lat': {'description': _('Latitude'), 'example_value': 48.833708},
}) 'lon': {'description': _('Longitude'), 'example_value': 2.323349},
},
)
def reverse(self, request, lat, lon, **kwargs): def reverse(self, request, lat, lon, **kwargs):
if kwargs.get('format', 'json') != 'json': if kwargs.get('format', 'json') != 'json':
raise NotImplementedError() raise NotImplementedError()
@ -196,18 +225,18 @@ class BaseAdresse(BaseResource):
break break
return result return result
@endpoint(description=_('Streets from zipcode'), @endpoint(
parameters={ description=_('Streets from zipcode'),
'id': {'description': _('Street identifier')}, parameters={
'q': {'description': _("Street name")}, 'id': {'description': _('Street identifier')},
'zipcode': {'description': _('Zipcode')}, 'q': {'description': _("Street name")},
'citycode': {'description': _('INSEE City code')}, 'zipcode': {'description': _('Zipcode')},
'page_limit': {'description': _('Maximum number of results to return'), 'citycode': {'description': _('INSEE City code')},
'example_value': 30}, 'page_limit': {'description': _('Maximum number of results to return'), 'example_value': 30},
'distinct': {'description': _('Remove duplicate streets')}, 'distinct': {'description': _('Remove duplicate streets')},
}) },
def streets(self, request, zipcode=None, citycode=None, )
q=None, id=None, distinct=True, page_limit=None): def streets(self, request, zipcode=None, citycode=None, q=None, id=None, distinct=True, page_limit=None):
result = [] result = []
if id is not None: if id is not None:
try: try:
@ -234,29 +263,38 @@ class BaseAdresse(BaseResource):
streets = streets[:page_limit] streets = streets[:page_limit]
for street in streets: for street in streets:
result.append({'id': str(street.id), result.append(
'text': street.name, {
'type': street.type, 'id': str(street.id),
'city': street.city, 'text': street.name,
'citycode': street.citycode, 'type': street.type,
'zipcode': street.zipcode}) 'city': street.city,
'citycode': street.citycode,
'zipcode': street.zipcode,
}
)
return {'data': result} return {'data': result}
@endpoint(description=_('Cities list'), @endpoint(
parameters={ description=_('Cities list'),
'id': {'description': _('Get exactly one city using its code and postal code ' parameters={
'separated with a dot'), 'id': {
'example_value': '75056.75014'}, 'description': _(
'q': {'description': _("Search text in name or postal code"), 'Get exactly one city using its code and postal code ' 'separated with a dot'
'example_value': 'Paris'}, ),
'code': {'description': _('INSEE code (or multiple codes separated with commas)'), 'example_value': '75056.75014',
'example_value': '75056'}, },
'region_code': {'description': _('Region code'), 'example_value': '11'}, 'q': {'description': _("Search text in name or postal code"), 'example_value': 'Paris'},
'department_code': {'description': _('Department code'), 'example_value': '75'}, 'code': {
}) 'description': _('INSEE code (or multiple codes separated with commas)'),
def cities(self, request, id=None, q=None, code=None, region_code=None, 'example_value': '75056',
department_code=None): },
'region_code': {'description': _('Region code'), 'example_value': '11'},
'department_code': {'description': _('Department code'), 'example_value': '75'},
},
)
def cities(self, request, id=None, q=None, code=None, region_code=None, department_code=None):
cities = CityModel.objects.all() cities = CityModel.objects.all()
if id is not None: if id is not None:
@ -267,8 +305,9 @@ class BaseAdresse(BaseResource):
cities = cities.filter(code=code, zipcode=zipcode) cities = cities.filter(code=code, zipcode=zipcode)
if q: if q:
unaccented_q = simplify(q) unaccented_q = simplify(q)
cities = cities.filter(Q(unaccent_name__istartswith=unaccented_q) | cities = cities.filter(
Q(zipcode__istartswith=unaccented_q)) Q(unaccent_name__istartswith=unaccented_q) | Q(zipcode__istartswith=unaccented_q)
)
if code: if code:
if ',' in code: if ',' in code:
codes = [c.strip() for c in code.split(',')] codes = [c.strip() for c in code.split(',')]
@ -283,13 +322,14 @@ class BaseAdresse(BaseResource):
cities = cities.select_related('department', 'region') cities = cities.select_related('department', 'region')
return {'data': [city.to_json() for city in cities]} return {'data': [city.to_json() for city in cities]}
@endpoint(description=_('Departments list'), @endpoint(
parameters={ description=_('Departments list'),
'id': {'description': _('Get exactly one department using its code'), parameters={
'example_value': '59'}, 'id': {'description': _('Get exactly one department using its code'), 'example_value': '59'},
'q': {'description': _('Search text in name or code'), 'example_value': 'Nord'}, 'q': {'description': _('Search text in name or code'), 'example_value': 'Nord'},
'region_code': {'description': _('Region code'), 'example_value': '32'}, 'region_code': {'description': _('Region code'), 'example_value': '32'},
}) },
)
def departments(self, request, id=None, q=None, region_code=None): def departments(self, request, id=None, q=None, region_code=None):
departments = DepartmentModel.objects.all() departments = DepartmentModel.objects.all()
@ -297,21 +337,22 @@ class BaseAdresse(BaseResource):
departments = departments.filter(code=id) departments = departments.filter(code=id)
if q: if q:
unaccented_q = simplify(q) unaccented_q = simplify(q)
departments = departments.filter(Q(unaccent_name__istartswith=unaccented_q) | departments = departments.filter(
Q(code__istartswith=unaccented_q)) Q(unaccent_name__istartswith=unaccented_q) | Q(code__istartswith=unaccented_q)
)
if region_code: if region_code:
departments = departments.filter(region__code=region_code) departments = departments.filter(region__code=region_code)
departments = departments.select_related('region') departments = departments.select_related('region')
return {'data': [department.to_json() for department in departments]} return {'data': [department.to_json() for department in departments]}
@endpoint(description=_('Regions list'), @endpoint(
parameters={ description=_('Regions list'),
'id': {'description': _('Get exactly one region using its code'), parameters={
'example_value': '32'}, 'id': {'description': _('Get exactly one region using its code'), 'example_value': '32'},
'q': {'description': _('Search text in name or code'), 'q': {'description': _('Search text in name or code'), 'example_value': 'Hauts-de-France'},
'example_value': 'Hauts-de-France'}, },
}) )
def regions(self, request, id=None, q=None): def regions(self, request, id=None, q=None):
regions = RegionModel.objects.all() regions = RegionModel.objects.all()
@ -319,8 +360,9 @@ class BaseAdresse(BaseResource):
regions = regions.filter(code=id) regions = regions.filter(code=id)
if q: if q:
unaccented_q = simplify(q) unaccented_q = simplify(q)
regions = regions.filter(Q(unaccent_name__istartswith=unaccented_q) | regions = regions.filter(
Q(code__istartswith=unaccented_q)) Q(unaccent_name__istartswith=unaccented_q) | Q(code__istartswith=unaccented_q)
)
return {'data': [region.to_json() for region in regions]} return {'data': [region.to_json() for region in regions]}
@ -362,7 +404,10 @@ class BaseAdresse(BaseResource):
for department in departments: for department in departments:
ban_gz = self.requests.get( ban_gz = self.requests.get(
'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-{}.ndjson.gz'.format(department)) 'https://adresse.data.gouv.fr/data/ban/adresses/latest/addok/adresses-addok-{}.ndjson.gz'.format(
department
)
)
if ban_gz.status_code != 200: if ban_gz.status_code != 200:
continue continue
@ -386,7 +431,8 @@ class BaseAdresse(BaseResource):
'city': street_info['city'], 'city': street_info['city'],
'zipcode': street_info['postcode'], 'zipcode': street_info['postcode'],
'type': street_info['type'], 'type': street_info['type'],
}) },
)
if line is _not_found: if line is _not_found:
raise Exception('bano file is empty') raise Exception('bano file is empty')
@ -409,8 +455,7 @@ class BaseAdresse(BaseResource):
except ValueError: except ValueError:
error = 'invalid json, got: %s' % response.text error = 'invalid json, got: %s' % response.text
if error: if error:
self.logger.error('failed to update api geo data for endpoint %s: %s', self.logger.error('failed to update api geo data for endpoint %s: %s', endpoint, error)
endpoint, error)
return return
if not result: if not result:
raise Exception('api geo returns empty json') raise Exception('api geo returns empty json')
@ -449,8 +494,7 @@ class BaseAdresse(BaseResource):
defaults['department'] = DepartmentModel.objects.get(code=data['codeDepartement']) defaults['department'] = DepartmentModel.objects.get(code=data['codeDepartement'])
if data.get('codeRegion'): if data.get('codeRegion'):
defaults['region'] = RegionModel.objects.get(code=data['codeRegion']) defaults['region'] = RegionModel.objects.get(code=data['codeRegion'])
CityModel.objects.update_or_create( CityModel.objects.update_or_create(code=data['code'], zipcode=zipcode, defaults=defaults)
code=data['code'], zipcode=zipcode, defaults=defaults)
CityModel.objects.filter(last_update__lt=start_update).delete() CityModel.objects.filter(last_update__lt=start_update).delete()
def clean_addresses_cache(self): def clean_addresses_cache(self):
@ -478,7 +522,6 @@ class BaseAdresse(BaseResource):
class UnaccentNameMixin(object): class UnaccentNameMixin(object):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.unaccent_name = simplify(self.name) self.unaccent_name = simplify(self.name)
super(UnaccentNameMixin, self).save(*args, **kwargs) super(UnaccentNameMixin, self).save(*args, **kwargs)

View File

@ -6,4 +6,5 @@ from passerelle.apps.bdp.models import Bdp
class BdpAdmin(admin.ModelAdmin): class BdpAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)} prepopulated_fields = {'slug': ('title',)}
admin.site.register(Bdp, BdpAdmin) admin.site.register(Bdp, BdpAdmin)

View File

@ -14,16 +14,41 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Bdp', name='Bdp',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('service_url', models.CharField(help_text='BDP Web Service URL', max_length=128, verbose_name='Service URL')), (
'service_url',
models.CharField(
help_text='BDP Web Service URL', max_length=128, verbose_name='Service URL'
),
),
('username', models.CharField(max_length=128, verbose_name='Username', blank=True)), ('username', models.CharField(max_length=128, verbose_name='Username', blank=True)),
('password', models.CharField(max_length=128, verbose_name='Password', blank=True)), ('password', models.CharField(max_length=128, verbose_name='Password', blank=True)),
('verify_cert', models.BooleanField(default=True, verbose_name='Check HTTPS Certificate validity')), (
('keystore', models.FileField(help_text='Certificate and private key in PEM format', upload_to=b'bdp', null=True, verbose_name='Keystore', blank=True)), 'verify_cert',
('users', models.ManyToManyField(to='base.ApiUser', related_name='_bdp_users_+', related_query_name='+', blank=True)), models.BooleanField(default=True, verbose_name='Check HTTPS Certificate validity'),
),
(
'keystore',
models.FileField(
help_text='Certificate and private key in PEM format',
upload_to=b'bdp',
null=True,
verbose_name='Keystore',
blank=True,
),
),
(
'users',
models.ManyToManyField(
to='base.ApiUser', related_name='_bdp_users_+', related_query_name='+', blank=True
),
),
], ],
options={ options={
'verbose_name': 'BDP Web Service', 'verbose_name': 'BDP Web Service',

View File

@ -14,7 +14,13 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='bdp', model_name='bdp',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Debug Enabled', blank=True, choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Debug Enabled',
blank=True,
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,7 +14,12 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='bdp', model_name='bdp',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,7 +14,19 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='bdp', model_name='bdp',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -8,20 +8,21 @@ from django.utils.translation import ugettext_lazy as _
from passerelle.base.models import BaseResource from passerelle.base.models import BaseResource
class Bdp(BaseResource): class Bdp(BaseResource):
service_url = models.CharField(max_length=128, blank=False, service_url = models.CharField(
verbose_name=_('Service URL'), max_length=128, blank=False, verbose_name=_('Service URL'), help_text=_('BDP Web Service URL')
help_text=_('BDP Web Service URL')) )
username = models.CharField(max_length=128, blank=True, username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'))
verbose_name=_('Username')) password = models.CharField(max_length=128, blank=True, verbose_name=_('Password'))
password = models.CharField(max_length=128, blank=True, verify_cert = models.BooleanField(default=True, verbose_name=_('Check HTTPS Certificate validity'))
verbose_name=_('Password')) keystore = models.FileField(
verify_cert = models.BooleanField(default=True, upload_to='bdp',
verbose_name=_('Check HTTPS Certificate validity')) blank=True,
keystore = models.FileField(upload_to='bdp', null=True,
blank=True, null=True, verbose_name=_('Keystore'),
verbose_name=_('Keystore'), help_text=_('Certificate and private key in PEM format'),
help_text=_('Certificate and private key in PEM format')) )
category = _('Business Process Connectors') category = _('Business Process Connectors')
@ -40,16 +41,13 @@ class Bdp(BaseResource):
def get_api(self, endpoint, **params): def get_api(self, endpoint, **params):
options = self.requests_options() options = self.requests_options()
return requests.get(self.service_url + '/api/' + endpoint, return requests.get(self.service_url + '/api/' + endpoint, params=params, **options).json()
params=params, **options).json()
def post_api(self, endpoint, obj): def post_api(self, endpoint, obj):
data = json.dumps(obj) data = json.dumps(obj)
headers = {'Content-Type': 'application/json'} headers = {'Content-Type': 'application/json'}
options = self.requests_options() options = self.requests_options()
request = requests.post( request = requests.post(self.service_url + '/api/' + endpoint, data=data, headers=headers, **options)
self.service_url + '/api/' + endpoint,
data=data, headers=headers, **options)
result = { result = {
'status_code': request.status_code, 'status_code': request.status_code,
'x_request_id': request.headers.get('x-request-id'), 'x_request_id': request.headers.get('x-request-id'),

View File

@ -5,5 +5,9 @@ from .views import BdpDetailView, ResourcesView, PostAdherentView
urlpatterns = [ urlpatterns = [
url(r'^(?P<slug>[\w,-]+)/$', BdpDetailView.as_view(), name='bdp-view'), url(r'^(?P<slug>[\w,-]+)/$', BdpDetailView.as_view(), name='bdp-view'),
url(r'^(?P<slug>[\w,-]+)/(?P<resources>[\w,-]+)/$', ResourcesView.as_view(), name='bdp-resources'), url(r'^(?P<slug>[\w,-]+)/(?P<resources>[\w,-]+)/$', ResourcesView.as_view(), name='bdp-resources'),
url(r'^(?P<slug>[\w,-]+)/post/adherent/$', csrf_exempt(PostAdherentView.as_view()), name='bdp-post-adherent'), url(
r'^(?P<slug>[\w,-]+)/post/adherent/$',
csrf_exempt(PostAdherentView.as_view()),
name='bdp-post-adherent',
),
] ]

View File

@ -40,16 +40,20 @@ class PostAdherentView(View, SingleObjectMixin):
@utils.protected_api('can_access') @utils.protected_api('can_access')
@utils.to_json() @utils.to_json()
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
data = json_loads(request.body) # JSON w.c.s. formdata data = json_loads(request.body) # JSON w.c.s. formdata
date_de_naissance = data['fields'].get('date_de_naissance') date_de_naissance = data['fields'].get('date_de_naissance')
# force 1973-04-18T00:00:00Z # force 1973-04-18T00:00:00Z
date_de_naissance = date_de_naissance[:10] + 'T00:00:00Z' date_de_naissance = date_de_naissance[:10] + 'T00:00:00Z'
abonnements = data['fields'].get('abonnements_raw') or \ abonnements = (
data['fields'].get('abonnements_raw') or \ data['fields'].get('abonnements_raw')
request.GET.get('abonnements') or data['fields'].get('abonnements_raw')
bibliotheque_id = data['fields'].get('bibliotheque_raw') or \ or request.GET.get('abonnements')
data['fields'].get('bibliotheque') or \ )
request.GET.get('bibliotheque') bibliotheque_id = (
data['fields'].get('bibliotheque_raw')
or data['fields'].get('bibliotheque')
or request.GET.get('bibliotheque')
)
adherent = { adherent = {
'nom': data['fields'].get('nom'), 'nom': data['fields'].get('nom'),
'prenom': data['fields'].get('prenom'), 'prenom': data['fields'].get('prenom'),

View File

@ -18,11 +18,19 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CartaDSCS', name='CartaDSCS',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('slug', models.SlugField(unique=True, verbose_name='Identifier')), ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
('wsdl_base_url', models.URLField(help_text='ex: https://example.net/adscs/webservices/', verbose_name='WSDL Base URL')), (
'wsdl_base_url',
models.URLField(
help_text='ex: https://example.net/adscs/webservices/', verbose_name='WSDL Base URL'
),
),
('username', models.CharField(max_length=64, verbose_name='Username')), ('username', models.CharField(max_length=64, verbose_name='Username')),
('password', models.CharField(max_length=64, verbose_name='Password')), ('password', models.CharField(max_length=64, verbose_name='Password')),
('iv', models.CharField(max_length=16, verbose_name='Initialisation Vector')), ('iv', models.CharField(max_length=16, verbose_name='Initialisation Vector')),
@ -31,7 +39,15 @@ class Migration(migrations.Migration):
('ftp_username', models.CharField(max_length=64, verbose_name='FTP Username')), ('ftp_username', models.CharField(max_length=64, verbose_name='FTP Username')),
('ftp_password', models.CharField(max_length=64, verbose_name='FTP Password')), ('ftp_password', models.CharField(max_length=64, verbose_name='FTP Password')),
('ftp_client_name', models.CharField(max_length=64, verbose_name='FTP Client Name')), ('ftp_client_name', models.CharField(max_length=64, verbose_name='FTP Client Name')),
('users', models.ManyToManyField(blank=True, related_name='_cartadscs_users_+', related_query_name='+', to='base.ApiUser')), (
'users',
models.ManyToManyField(
blank=True,
related_name='_cartadscs_users_+',
related_query_name='+',
to='base.ApiUser',
),
),
], ],
options={ options={
'verbose_name': 'Cart@DS CS', 'verbose_name': 'Cart@DS CS',
@ -40,7 +56,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CartaDSDossier', name='CartaDSDossier',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('email', models.CharField(max_length=256)), ('email', models.CharField(max_length=256)),
('tracking_code', models.CharField(max_length=20)), ('tracking_code', models.CharField(max_length=20)),
('commune_id', models.CharField(max_length=20)), ('commune_id', models.CharField(max_length=20)),
@ -59,10 +78,16 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CartaDSFile', name='CartaDSFile',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('tracking_code', models.CharField(max_length=20)), ('tracking_code', models.CharField(max_length=20)),
('id_piece', models.CharField(max_length=20)), ('id_piece', models.CharField(max_length=20)),
('uploaded_file', models.FileField(upload_to=passerelle.apps.cartads_cs.models.cartads_file_location)), (
'uploaded_file',
models.FileField(upload_to=passerelle.apps.cartads_cs.models.cartads_file_location),
),
('last_update_datetime', models.DateTimeField(auto_now=True)), ('last_update_datetime', models.DateTimeField(auto_now=True)),
], ],
), ),

View File

@ -16,7 +16,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CartaDSDataCache', name='CartaDSDataCache',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('data_type', models.CharField(max_length=50)), ('data_type', models.CharField(max_length=50)),
('data_parameters', django.contrib.postgres.fields.jsonb.JSONField(default={})), ('data_parameters', django.contrib.postgres.fields.jsonb.JSONField(default={})),
('data_values', django.contrib.postgres.fields.jsonb.JSONField(default={})), ('data_values', django.contrib.postgres.fields.jsonb.JSONField(default={})),

View File

@ -15,6 +15,12 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='cartadscs', model_name='cartadscs',
name='client_name', name='client_name',
field=models.CharField(blank=True, help_text='Only useful in shared environments.', max_length=64, null=True, verbose_name='Client Name'), field=models.CharField(
blank=True,
help_text='Only useful in shared environments.',
max_length=64,
null=True,
verbose_name='Client Name',
),
), ),
] ]

View File

@ -15,7 +15,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CartaDSSubscriber', name='CartaDSSubscriber',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name_id', models.CharField(max_length=32, null=True)), ('name_id', models.CharField(max_length=32, null=True)),
], ],
), ),

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='ChoositRegisterGateway', name='ChoositRegisterGateway',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
@ -30,7 +33,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='ChoositRegisterNewsletter', name='ChoositRegisterNewsletter',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('name', models.CharField(max_length=16)), ('name', models.CharField(max_length=16)),
('description', models.CharField(max_length=128, blank=True)), ('description', models.CharField(max_length=128, blank=True)),
], ],
@ -43,13 +49,27 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='ChoositSMSGateway', name='ChoositSMSGateway',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('key', models.CharField(max_length=64, verbose_name='Key')), ('key', models.CharField(max_length=64, verbose_name='Key')),
('default_country_code', models.CharField(default='33', max_length=3, verbose_name='Default country code')), (
('users', models.ManyToManyField(to='base.ApiUser', related_name='_choositsmsgateway_users_+', related_query_name='+', blank=True)), 'default_country_code',
models.CharField(default='33', max_length=3, verbose_name='Default country code'),
),
(
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_choositsmsgateway_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'db_table': 'sms_choosit', 'db_table': 'sms_choosit',

View File

@ -14,13 +14,25 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='choositregistergateway', model_name='choositregistergateway',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Debug Enabled', blank=True, choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Debug Enabled',
blank=True,
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
migrations.AddField( migrations.AddField(
model_name='choositsmsgateway', model_name='choositsmsgateway',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Debug Enabled', blank=True, choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Debug Enabled',
blank=True,
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,13 +14,23 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='choositregistergateway', model_name='choositregistergateway',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
migrations.AlterField( migrations.AlterField(
model_name='choositsmsgateway', model_name='choositsmsgateway',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,13 +14,37 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='choositregistergateway', model_name='choositregistergateway',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
preserve_default=True, preserve_default=True,
), ),
migrations.AlterField( migrations.AlterField(
model_name='choositsmsgateway', model_name='choositsmsgateway',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -26,8 +26,8 @@ class ChoositSMSGateway(SMSResource):
'data': [ 'data': [
[u'0033688888888', u'Choosit error: bad JSON response'], [u'0033688888888', u'Choosit error: bad JSON response'],
[u'0033677777777', u'Choosit error: bad JSON response'], [u'0033677777777', u'Choosit error: bad JSON response'],
] ],
} },
}, },
{ {
'response': { 'response': {
@ -40,7 +40,7 @@ class ChoositSMSGateway(SMSResource):
[u'0033688888888', u'Choosit error: not ok'], [u'0033688888888', u'Choosit error: not ok'],
[u'0033677777777', u'Choosit error: not ok'], [u'0033677777777', u'Choosit error: not ok'],
], ],
} },
}, },
{ {
'response': { 'response': {
@ -53,9 +53,8 @@ class ChoositSMSGateway(SMSResource):
[u'0033688888888', {'result': u'Envoi terminé', 'sms_id': 1234}], [u'0033688888888', {'result': u'Envoi terminé', 'sms_id': 1234}],
[u'0033677777777', {'result': u'Envoi terminé', 'sms_id': 1234}], [u'0033677777777', {'result': u'Envoi terminé', 'sms_id': 1234}],
], ],
} },
} },
], ],
} }
URL = 'http://sms.choosit.com/webservice' URL = 'http://sms.choosit.com/webservice'
@ -97,6 +96,5 @@ class ChoositSMSGateway(SMSResource):
else: else:
results.append(output) results.append(output)
if any(isinstance(result, string_types) for result in results): if any(isinstance(result, string_types) for result in results):
raise APIError('Choosit error: some destinations failed', raise APIError('Choosit error: some destinations failed', data=list(zip(destinations, results)))
data=list(zip(destinations, results)))
return list(zip(destinations, results)) return list(zip(destinations, results))

View File

@ -32,37 +32,30 @@ CERTIFICATE_TYPES = [
{"id": "NAI", "text": "Naissance"}, {"id": "NAI", "text": "Naissance"},
{"id": "MAR", "text": "Mariage"}, {"id": "MAR", "text": "Mariage"},
{"id": "REC", "text": "Reconnaissance"}, {"id": "REC", "text": "Reconnaissance"},
{"id": "DEC", "text": "Décès"} {"id": "DEC", "text": "Décès"},
] ]
SEXES = [ SEXES = [{"id": "M", "text": "Homme"}, {"id": "F", "text": "Femme"}, {"id": "NA", "text": "Autre"}]
{"id": "M", "text": "Homme"},
{"id": "F", "text": "Femme"},
{"id": "NA", "text": "Autre"}
]
TITLES = [ TITLES = [
{"id": "M", "text": "Monsieur"}, {"id": "M", "text": "Monsieur"},
{"id": "Mme", "text": "Madame"}, {"id": "Mme", "text": "Madame"},
{"id": "Mlle", "text": "Mademoiselle"} {"id": "Mlle", "text": "Mademoiselle"},
] ]
DOCUMENT_TYPES = [ DOCUMENT_TYPES = [
{"id": "CPI", "text": "Copie intégrale"}, {"id": "CPI", "text": "Copie intégrale"},
{"id": "EXTAF", "text": "Extrait avec filiation"}, {"id": "EXTAF", "text": "Extrait avec filiation"},
{"id": "EXTSF", "text": "Extrait sans filiation"}, {"id": "EXTSF", "text": "Extrait sans filiation"},
{"id": "EXTPL", "text": "Extrait plurilingue"} {"id": "EXTPL", "text": "Extrait plurilingue"},
] ]
CONCERNED = [ CONCERNED = [{"id": "reconnu", "text": "Reconnu"}, {"id": "auteur", "text": "Auteur"}]
{"id": "reconnu", "text": "Reconnu"},
{"id": "auteur", "text": "Auteur"}
]
ORIGINS = [ ORIGINS = [
{"id": "internet", "text": "Internet"}, {"id": "internet", "text": "Internet"},
{"id": "guichet", "text": "Guichet"}, {"id": "guichet", "text": "Guichet"},
{"id": "courrier", "text": "Courrier"} {"id": "courrier", "text": "Courrier"},
] ]
@ -73,8 +66,8 @@ def is_clean(element):
class BaseType(object): class BaseType(object):
"""Base data binding object """Base data binding object"""
"""
tagname = None tagname = None
def __repr__(self): def __repr__(self):
@ -82,8 +75,7 @@ class BaseType(object):
@classmethod @classmethod
def make_element(cls, tagname, value=None, namespace=None, nsmap=None): def make_element(cls, tagname, value=None, namespace=None, nsmap=None):
M = xobject.ElementMaker(annotate=False, namespace=namespace, M = xobject.ElementMaker(annotate=False, namespace=namespace, nsmap=nsmap)
nsmap=nsmap)
return M(tagname, value) return M(tagname, value)
@property @property
@ -115,19 +107,17 @@ class CityWebType(BaseType):
class SimpleType(CityWebType): class SimpleType(CityWebType):
"""Data binding class for SimpleType """Data binding class for SimpleType"""
"""
allowed_values = None allowed_values = None
def __init__(self, value): def __init__(self, value):
if value not in self.allowed_values: if value not in self.allowed_values:
raise APIError('<%s> value (%s) not in %s' % (self.tagname, value, raise APIError('<%s> value (%s) not in %s' % (self.tagname, value, self.allowed_values))
self.allowed_values))
self.value = value self.value = value
class DateType(CityWebType): class DateType(CityWebType):
def __init__(self, value): def __init__(self, value):
try: try:
self.value = parse_date(value) self.value = parse_date(value)
@ -139,8 +129,8 @@ class DateType(CityWebType):
class ComplexType(CityWebType): class ComplexType(CityWebType):
"""Data binding class for ComplexType """Data binding class for ComplexType"""
"""
sequence = None sequence = None
pattern = None pattern = None
@ -229,8 +219,7 @@ class Place(ComplexType):
class Address(ComplexType): class Address(ComplexType):
tagname = 'adresse' tagname = 'adresse'
sequence = ('ligneAdr1', 'ligneAdr2', 'codePostal', sequence = ('ligneAdr1', 'ligneAdr2', 'codePostal', 'lieu', 'mail', 'tel')
'lieu', 'mail', 'tel')
pattern = 'address_' pattern = 'address_'
def __init__(self, data): def __init__(self, data):
@ -273,8 +262,7 @@ class EventPlace(Place):
class Person(ComplexType): class Person(ComplexType):
sequence = ('noms', 'prenoms', 'genre', 'adresse', 'sexe', sequence = ('noms', 'prenoms', 'genre', 'adresse', 'sexe', 'pere', 'mere', 'naissance')
'pere', 'mere', 'naissance')
def __init__(self, data): def __init__(self, data):
super(Person, self).__init__(data) super(Person, self).__init__(data)
@ -314,8 +302,7 @@ class Parent(Person):
class ConcernedCommon(Person): class ConcernedCommon(Person):
sequence = ('noms', 'prenoms', 'genre', 'sexe', sequence = ('noms', 'prenoms', 'genre', 'sexe', 'parent1', 'parent2', 'naissance')
'parent1', 'parent2', 'naissance')
def __init__(self, data): def __init__(self, data):
super(ConcernedCommon, self).__init__(data) super(ConcernedCommon, self).__init__(data)
@ -344,8 +331,7 @@ class Applicant(ComplexType):
class Event(ComplexType): class Event(ComplexType):
tagname = 'evenement' tagname = 'evenement'
sequence = ('interesse', 'conjoint', 'natureEvenement', sequence = ('interesse', 'conjoint', 'natureEvenement', 'typeInteresse', 'dateEvenement', 'lieuEvenement')
'typeInteresse', 'dateEvenement', 'lieuEvenement')
def __init__(self, data): def __init__(self, data):
certificate_type = data['certificate_type'] certificate_type = data['certificate_type']
@ -362,8 +348,16 @@ class Event(ComplexType):
class CivilStatusApplication(ComplexType): class CivilStatusApplication(ComplexType):
tagname = 'demandeEtatCivil' tagname = 'demandeEtatCivil'
sequence = ( sequence = (
'identifiant', 'demandeur', 'natureDocument', 'nbExemplaire', 'identifiant',
'dateDemande', 'evenement', 'motif', 'origine', 'commentaire') 'demandeur',
'natureDocument',
'nbExemplaire',
'dateDemande',
'evenement',
'motif',
'origine',
'commentaire',
)
def __init__(self, data): def __init__(self, data):
self.identifiant = data['application_id'] self.identifiant = data['application_id']
@ -388,5 +382,5 @@ class CivilStatusApplication(ComplexType):
with atomic_write(filepath) as fd: with atomic_write(filepath) as fd:
fd.write(force_bytes(content)) fd.write(force_bytes(content))
# set read only permission for owner and group # set read only permission for owner and group
os.chmod(filepath, stat.S_IRUSR|stat.S_IRGRP) os.chmod(filepath, stat.S_IRUSR | stat.S_IRGRP)
return filename return filename

View File

@ -14,12 +14,35 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CityWeb', name='CityWeb',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('log_level', models.CharField(default=b'INFO', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')])), (
('users', models.ManyToManyField(to='base.ApiUser', related_name='_cityweb_users_+', related_query_name='+', blank=True)), 'log_level',
models.CharField(
default=b'INFO',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
),
(
'users',
models.ManyToManyField(
to='base.ApiUser', related_name='_cityweb_users_+', related_query_name='+', blank=True
),
),
], ],
options={ options={
'verbose_name': "CityWeb - Demande d'acte d'\xe9tat civil", 'verbose_name': "CityWeb - Demande d'acte d'\xe9tat civil",

View File

@ -24,8 +24,15 @@ from passerelle.compat import json_loads
from passerelle.utils.api import endpoint from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError from passerelle.utils.jsonresponse import APIError
from .cityweb import (CivilStatusApplication, TITLES, SEXES, DOCUMENT_TYPES, from .cityweb import (
CERTIFICATE_TYPES, CONCERNED, ORIGINS) CivilStatusApplication,
TITLES,
SEXES,
DOCUMENT_TYPES,
CERTIFICATE_TYPES,
CONCERNED,
ORIGINS,
)
class CityWeb(BaseResource): class CityWeb(BaseResource):
@ -52,8 +59,7 @@ class CityWeb(BaseResource):
@property @property
def basepath(self): def basepath(self):
return os.path.join( return os.path.join(default_storage.path('cityweb'), self.slug)
default_storage.path('cityweb'), self.slug)
@endpoint(perm='can_access', description=_('Get title list')) @endpoint(perm='can_access', description=_('Get title list'))
def titles(self, request): def titles(self, request):
@ -71,14 +77,20 @@ class CityWeb(BaseResource):
def origins(self, request): def origins(self, request):
return {'data': ORIGINS} return {'data': ORIGINS}
@endpoint(name='certificate-types', perm='can_access', @endpoint(
description=_('Get certificate type list'), parameters={'exclude': {'example_value': 'REC'}}) name='certificate-types',
perm='can_access',
description=_('Get certificate type list'),
parameters={'exclude': {'example_value': 'REC'}},
)
def certificate_types(self, request, exclude=''): def certificate_types(self, request, exclude=''):
return {'data': [item for item in CERTIFICATE_TYPES return {'data': [item for item in CERTIFICATE_TYPES if item.get('id') not in exclude.split(',')]}
if item.get('id') not in exclude.split(',')]}
@endpoint(name='document-types', perm='can_access', @endpoint(
description=_('Get document type list'), parameters={'exclude': {'example_value': 'EXTPL'}}) name='document-types',
perm='can_access',
description=_('Get document type list'),
parameters={'exclude': {'example_value': 'EXTPL'}},
)
def document_types(self, request, exclude=''): def document_types(self, request, exclude=''):
return {'data': [item for item in DOCUMENT_TYPES return {'data': [item for item in DOCUMENT_TYPES if item.get('id') not in exclude.split(',')]}
if item.get('id') not in exclude.split(',')]}

View File

@ -6,8 +6,6 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = []
]
operations = [ operations = []
]

View File

@ -7,7 +7,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
replaces = [('clicrdv', '0001_initial'), ('clicrdv', '0002_clicrdv_group_id'), ('clicrdv', '0003_auto_20160920_0903'), ('clicrdv', '0004_newclicrdv'), ('clicrdv', '0005_auto_20161218_1701'), ('clicrdv', '0006_auto_20170920_0951')] replaces = [
('clicrdv', '0001_initial'),
('clicrdv', '0002_clicrdv_group_id'),
('clicrdv', '0003_auto_20160920_0903'),
('clicrdv', '0004_newclicrdv'),
('clicrdv', '0005_auto_20161218_1701'),
('clicrdv', '0006_auto_20170920_0951'),
]
initial = True initial = True
@ -20,19 +27,59 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='ClicRdv', name='ClicRdv',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('log_level', models.CharField(choices=[('NOTSET', 'NOTSET'), ('DEBUG', 'DEBUG'), ('INFO', 'INFO'), ('WARNING', 'WARNING'), ('ERROR', 'ERROR'), ('CRITICAL', 'CRITICAL')], default='INFO', max_length=10, verbose_name='Log Level')), (
('server', models.CharField(choices=[('www.clicrdv.com', 'Production (www.clicrdv.com)'), ('sandbox.clicrdv.com', 'SandBox (sandbox.clicrdv.com)')], default='sandbox.clicrdv.com', max_length=64, verbose_name='Server')), 'log_level',
models.CharField(
choices=[
('NOTSET', 'NOTSET'),
('DEBUG', 'DEBUG'),
('INFO', 'INFO'),
('WARNING', 'WARNING'),
('ERROR', 'ERROR'),
('CRITICAL', 'CRITICAL'),
],
default='INFO',
max_length=10,
verbose_name='Log Level',
),
),
(
'server',
models.CharField(
choices=[
('www.clicrdv.com', 'Production (www.clicrdv.com)'),
('sandbox.clicrdv.com', 'SandBox (sandbox.clicrdv.com)'),
],
default='sandbox.clicrdv.com',
max_length=64,
verbose_name='Server',
),
),
('group_id', models.IntegerField(default=0, verbose_name='Group Id')), ('group_id', models.IntegerField(default=0, verbose_name='Group Id')),
('apikey', models.CharField(max_length=64, verbose_name='API Key')), ('apikey', models.CharField(max_length=64, verbose_name='API Key')),
('username', models.CharField(max_length=64, verbose_name='Username')), ('username', models.CharField(max_length=64, verbose_name='Username')),
('password', models.CharField(max_length=64, verbose_name='Password')), ('password', models.CharField(max_length=64, verbose_name='Password')),
('websource', models.CharField(blank=True, max_length=64, null=True, verbose_name='Web source')), (
('default_comment', models.CharField(blank=True, max_length=250, null=True, verbose_name='Default comment')), 'websource',
('users', models.ManyToManyField(blank=True, related_name='_clicrdv_users_+', related_query_name='+', to='base.ApiUser')), models.CharField(blank=True, max_length=64, null=True, verbose_name='Web source'),
),
(
'default_comment',
models.CharField(blank=True, max_length=250, null=True, verbose_name='Default comment'),
),
(
'users',
models.ManyToManyField(
blank=True, related_name='_clicrdv_users_+', related_query_name='+', to='base.ApiUser'
),
),
], ],
options={ options={
'verbose_name': 'Clicrdv Agenda', 'verbose_name': 'Clicrdv Agenda',

View File

@ -10,5 +10,4 @@ class Migration(migrations.Migration):
('clicrdv', '0001_initial'), ('clicrdv', '0001_initial'),
] ]
operations = [ operations = []
]

View File

@ -7,13 +7,13 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('clicrdv', '0001_squashed_0006_auto_20170920_0951'), ('clicrdv', '0001_squashed_0006_auto_20170920_0951'),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='clicrdv', model_name='clicrdv',
name='log_level', name='log_level',
), ),
] ]

View File

@ -10,5 +10,4 @@ class Migration(migrations.Migration):
('clicrdv', '0002_clicrdv_group_id'), ('clicrdv', '0002_clicrdv_group_id'),
] ]
operations = [ operations = []
]

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
@ -15,19 +16,56 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='NewClicRdv', name='NewClicRdv',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier')), ('slug', models.SlugField(verbose_name='Identifier')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('log_level', models.CharField(default='NOTSET', max_length=10, verbose_name='Log Level', choices=[('NOTSET', 'NOTSET'), ('DEBUG', 'DEBUG'), ('INFO', 'INFO'), ('WARNING', 'WARNING'), ('ERROR', 'ERROR'), ('CRITICAL', 'CRITICAL'), ('FATAL', 'FATAL')])), (
('server', models.CharField(default='sandbox.clicrdv.com', max_length=64, choices=[('www.clicrdv.com', 'Production (www.clicrdv.com)'), ('sandbox.clicrdv.com', 'SandBox (sandbox.clicrdv.com)')])), 'log_level',
models.CharField(
default='NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[
('NOTSET', 'NOTSET'),
('DEBUG', 'DEBUG'),
('INFO', 'INFO'),
('WARNING', 'WARNING'),
('ERROR', 'ERROR'),
('CRITICAL', 'CRITICAL'),
('FATAL', 'FATAL'),
],
),
),
(
'server',
models.CharField(
default='sandbox.clicrdv.com',
max_length=64,
choices=[
('www.clicrdv.com', 'Production (www.clicrdv.com)'),
('sandbox.clicrdv.com', 'SandBox (sandbox.clicrdv.com)'),
],
),
),
('group_id', models.IntegerField(default=0)), ('group_id', models.IntegerField(default=0)),
('apikey', models.CharField(max_length=64)), ('apikey', models.CharField(max_length=64)),
('username', models.CharField(max_length=64)), ('username', models.CharField(max_length=64)),
('password', models.CharField(max_length=64)), ('password', models.CharField(max_length=64)),
('websource', models.CharField(max_length=64, null=True, blank=True)), ('websource', models.CharField(max_length=64, null=True, blank=True)),
('default_comment', models.CharField(max_length=250, null=True, blank=True)), ('default_comment', models.CharField(max_length=250, null=True, blank=True)),
('users', models.ManyToManyField(to='base.ApiUser', related_name='_newclicrdv_users_+', related_query_name='+', blank=True)), (
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_newclicrdv_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'verbose_name': 'Clicrdv Agenda', 'verbose_name': 'Clicrdv Agenda',

View File

@ -22,15 +22,14 @@ from passerelle.utils.api import endpoint
CLICRDV_SERVERS = ( CLICRDV_SERVERS = (
('www.clicrdv.com', 'Production (www.clicrdv.com)'), ('www.clicrdv.com', 'Production (www.clicrdv.com)'),
('sandbox.clicrdv.com', 'SandBox (sandbox.clicrdv.com)') ('sandbox.clicrdv.com', 'SandBox (sandbox.clicrdv.com)'),
) )
class ClicRdv(BaseResource): class ClicRdv(BaseResource):
server = models.CharField( server = models.CharField(
_('Server'), _('Server'), max_length=64, choices=CLICRDV_SERVERS, default='sandbox.clicrdv.com'
max_length=64, )
choices=CLICRDV_SERVERS,
default='sandbox.clicrdv.com')
group_id = models.IntegerField(_('Group Id'), default=0) group_id = models.IntegerField(_('Group Id'), default=0)
apikey = models.CharField(_('API Key'), max_length=64) apikey = models.CharField(_('API Key'), max_length=64)
username = models.CharField(_('Username'), max_length=64) username = models.CharField(_('Username'), max_length=64)
@ -120,8 +119,7 @@ class ClicRdv(BaseResource):
datetimes = [] datetimes = []
for timeslot in self.get_available_timeslots(intervention): for timeslot in self.get_available_timeslots(intervention):
parsed = datetime.datetime.strptime(timeslot, '%Y-%m-%d %H:%M:%S') parsed = datetime.datetime.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
datetimed = {'id': parsed.strftime('%Y-%m-%d-%H:%M:%S'), datetimed = {'id': parsed.strftime('%Y-%m-%d-%H:%M:%S'), 'text': date_format(parsed, 'j F Y H:i')}
'text': date_format(parsed, 'j F Y H:i')}
datetimes.append(datetimed) datetimes.append(datetimed)
datetimes.sort(key=lambda x: x.get('id')) datetimes.sort(key=lambda x: x.get('id'))
return datetimes return datetimes
@ -130,8 +128,7 @@ class ClicRdv(BaseResource):
dates = [] dates = []
for timeslot in self.get_available_timeslots(intervention): for timeslot in self.get_available_timeslots(intervention):
parsed = datetime.datetime.strptime(timeslot, '%Y-%m-%d %H:%M:%S') parsed = datetime.datetime.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
date = {'id': parsed.strftime('%Y-%m-%d'), date = {'id': parsed.strftime('%Y-%m-%d'), 'text': date_format(parsed, 'j F Y')}
'text': date_format(parsed, 'j F Y')}
if date in dates: if date in dates:
continue continue
dates.append(date) dates.append(date)
@ -142,12 +139,11 @@ class ClicRdv(BaseResource):
if not date: if not date:
raise Exception('no date value') raise Exception('no date value')
times = [] times = []
for timeslot in self.get_available_timeslots(intervention, for timeslot in self.get_available_timeslots(
date_start='%s 00:00:00' % date, intervention, date_start='%s 00:00:00' % date, date_end='%s 23:59:59' % date
date_end='%s 23:59:59' % date): ):
parsed = datetime.datetime.strptime(timeslot, '%Y-%m-%d %H:%M:%S') parsed = datetime.datetime.strptime(timeslot, '%Y-%m-%d %H:%M:%S')
timed = {'id': parsed.strftime('%H:%M:%S'), timed = {'id': parsed.strftime('%H:%M:%S'), 'text': time_format(parsed, 'H:i')}
'text': time_format(parsed, 'H:i')}
times.append(timed) times.append(timed)
times.sort(key=lambda x: x.get('id')) times.sort(key=lambda x: x.get('id'))
return times return times
@ -158,12 +154,13 @@ class ClicRdv(BaseResource):
return response return response
return {'success': True} return {'success': True}
def create_appointment(self, intervention, websource, data): def create_appointment(self, intervention, websource, data):
fields = data.get('fields') or {} fields = data.get('fields') or {}
extra = data.get('extra') or {} extra = data.get('extra') or {}
def get_data(key, default=None): def get_data(key, default=None):
return data.get(key) or extra.get(key) or fields.get(key) or default return data.get(key) or extra.get(key) or fields.get(key) or default
if intervention: if intervention:
intervention = int(intervention) intervention = int(intervention)
else: else:
@ -184,17 +181,17 @@ class ClicRdv(BaseResource):
'email': get_data('clicrdv_email', ''), 'email': get_data('clicrdv_email', ''),
'firstphone': get_data('clicrdv_firstphone', ''), 'firstphone': get_data('clicrdv_firstphone', ''),
'secondphone': get_data('clicrdv_secondphone', ''), 'secondphone': get_data('clicrdv_secondphone', ''),
}, },
'date': date, 'date': date,
'intervention_ids': [intervention], 'intervention_ids': [intervention],
'websource': websource, 'websource': websource,
}, },
} }
comments = get_data('clicrdv_comments') or self.default_comment comments = get_data('clicrdv_comments') or self.default_comment
if comments: if comments:
appointment['comments'] = comments appointment['comments'] = comments
# optional parameters, if any... # optional parameters, if any...
for fieldname in (list(fields.keys()) + list(extra.keys()) + list(data.keys())): for fieldname in list(fields.keys()) + list(extra.keys()) + list(data.keys()):
if fieldname.startswith('clicrdv_fiche_'): if fieldname.startswith('clicrdv_fiche_'):
appointment['appointment']['fiche'][fieldname[14:]] = get_data(fieldname) or '' appointment['appointment']['fiche'][fieldname[14:]] = get_data(fieldname) or ''
response = self.request('appointments', 'post', json=appointment) response = self.request('appointments', 'post', json=appointment)

View File

@ -7,17 +7,34 @@ from passerelle.apps.clicrdv.views import *
urlpatterns = [ urlpatterns = [
url(r'^(?P<slug>[\w,-]+)/$', ClicRdvDetailView.as_view(), name='clicrdv-view'), url(r'^(?P<slug>[\w,-]+)/$', ClicRdvDetailView.as_view(), name='clicrdv-view'),
url(
url(r'^(?P<slug>[\w,-]+)/interventions/(?P<intervention_id>\d+)/datetimes/$', r'^(?P<slug>[\w,-]+)/interventions/(?P<intervention_id>\d+)/datetimes/$',
DateTimesView.as_view(), name='clicrdv-datetimes'), DateTimesView.as_view(),
url(r'^(?P<slug>[\w,-]+)/interventions/(?P<intervention_id>\d+)/dates/$', name='clicrdv-datetimes',
DatesView.as_view(), name='clicrdv-dates'), ),
url(r'^(?P<slug>[\w,-]+)/interventions/(?P<intervention_id>\d+)/(?P<date>[\d-]+)/times$', url(
TimesView.as_view(), name='clicrdv-times'), r'^(?P<slug>[\w,-]+)/interventions/(?P<intervention_id>\d+)/dates/$',
url(r'^(?P<slug>[\w,-]+)/interventions/(?P<intervention_id>\d+)/create$', DatesView.as_view(),
csrf_exempt(CreateAppointmentView.as_view()), name='clicrdv-create-appointment'), name='clicrdv-dates',
url(r'^(?P<slug>[\w,-]+)/create$', ),
csrf_exempt(CreateAppointmentView.as_view()), name='clicrdv-create-appointment-qs'), url(
url(r'^(?P<slug>[\w,-]+)/(?P<appointment_id>\d+)/cancel$', r'^(?P<slug>[\w,-]+)/interventions/(?P<intervention_id>\d+)/(?P<date>[\d-]+)/times$',
CancelAppointmentView.as_view(), name='clicrdv-cancel-appointment'), TimesView.as_view(),
name='clicrdv-times',
),
url(
r'^(?P<slug>[\w,-]+)/interventions/(?P<intervention_id>\d+)/create$',
csrf_exempt(CreateAppointmentView.as_view()),
name='clicrdv-create-appointment',
),
url(
r'^(?P<slug>[\w,-]+)/create$',
csrf_exempt(CreateAppointmentView.as_view()),
name='clicrdv-create-appointment-qs',
),
url(
r'^(?P<slug>[\w,-]+)/(?P<appointment_id>\d+)/cancel$',
CancelAppointmentView.as_view(),
name='clicrdv-cancel-appointment',
),
] ]

View File

@ -16,6 +16,7 @@ class DateTimesView(View, SingleObjectMixin):
input: https//passerelle/clicrdv/foobar/interventions/887/datetimes input: https//passerelle/clicrdv/foobar/interventions/887/datetimes
""" """
model = ClicRdv model = ClicRdv
@utils.to_json() @utils.to_json()
@ -31,6 +32,7 @@ class DatesView(View, SingleObjectMixin):
{ data: [ { id: '2014-05-07', text: "7 mai 2014" }, { data: [ { id: '2014-05-07', text: "7 mai 2014" },
{ id: '2014-05-13', text: "13 mai 2014" } ], err: 0 } { id: '2014-05-13', text: "13 mai 2014" } ], err: 0 }
""" """
model = ClicRdv model = ClicRdv
@utils.to_json() @utils.to_json()
@ -46,6 +48,7 @@ class TimesView(View, SingleObjectMixin):
{ data: [ { id: '15:10:00', text: "15:10" }, { data: [ { id: '15:10:00', text: "15:10" },
{ id: '15:30:00', text: "15:30" } ], err: 0 } { id: '15:30:00', text: "15:30" } ], err: 0 }
""" """
model = ClicRdv model = ClicRdv
@utils.to_json() @utils.to_json()
@ -77,6 +80,7 @@ class CreateAppointmentView(View, SingleObjectMixin):
output: output:
{ data: { 'success': true, 'appointment_id': 123 }, err: 0 } { data: { 'success': true, 'appointment_id': 123 }, err: 0 }
""" """
model = ClicRdv model = ClicRdv
@utils.protected_api('can_manage_appointment') @utils.protected_api('can_manage_appointment')
@ -85,10 +89,11 @@ class CreateAppointmentView(View, SingleObjectMixin):
if intervention_id is None: if intervention_id is None:
intervention_id = self.request.GET.get('intervention') intervention_id = self.request.GET.get('intervention')
data = json_loads(request.body) data = json_loads(request.body)
return {'data': self.get_object().create_appointment( return {
intervention_id, 'data': self.get_object().create_appointment(
self.request.GET.get('websource'), intervention_id, self.request.GET.get('websource'), data
data)} )
}
class CancelAppointmentView(View, SingleObjectMixin): class CancelAppointmentView(View, SingleObjectMixin):
@ -98,6 +103,7 @@ class CancelAppointmentView(View, SingleObjectMixin):
output: output:
{ data: { 'success': true }, err: 0 } { data: { 'success': true }, err: 0 }
""" """
model = ClicRdv model = ClicRdv
@utils.protected_api('can_manage_appointment') @utils.protected_api('can_manage_appointment')

View File

@ -14,15 +14,48 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CmisConnector', name='CmisConnector',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('log_level', models.CharField(default=b'INFO', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')])), (
('cmis_endpoint', models.URLField(help_text='URL of the CMIS Atom endpoint', max_length=400, verbose_name='CMIS Atom endpoint')), 'log_level',
models.CharField(
default=b'INFO',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
),
(
'cmis_endpoint',
models.URLField(
help_text='URL of the CMIS Atom endpoint',
max_length=400,
verbose_name='CMIS Atom endpoint',
),
),
('username', models.CharField(max_length=128, verbose_name='Service username')), ('username', models.CharField(max_length=128, verbose_name='Service username')),
('password', models.CharField(max_length=128, verbose_name='Service password')), ('password', models.CharField(max_length=128, verbose_name='Service password')),
('users', models.ManyToManyField(to='base.ApiUser', related_name='_cmisconnector_users_+', related_query_name='+', blank=True)), (
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_cmisconnector_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'verbose_name': 'CMIS connector', 'verbose_name': 'CMIS connector',

View File

@ -54,7 +54,7 @@ UPLOAD_SCHEMA = {
'content': {'type': 'string'}, 'content': {'type': 'string'},
'content_type': {'type': 'string'}, 'content_type': {'type': 'string'},
}, },
'required': ['content'] 'required': ['content'],
}, },
'filename': { 'filename': {
'type': 'string', 'type': 'string',
@ -65,10 +65,7 @@ UPLOAD_SCHEMA = {
'pattern': FILE_PATH_PATTERN, 'pattern': FILE_PATH_PATTERN,
}, },
'object_type': {'type': 'string'}, 'object_type': {'type': 'string'},
'properties': { 'properties': {'type': 'object', 'additionalProperties': {'type': 'string'}},
'type': 'object',
'additionalProperties': {'type': 'string'}
},
}, },
'required': ['file', 'path'], 'required': ['file', 'path'],
'unflatten': True, 'unflatten': True,
@ -77,8 +74,8 @@ UPLOAD_SCHEMA = {
class CmisConnector(BaseResource): class CmisConnector(BaseResource):
cmis_endpoint = models.URLField( cmis_endpoint = models.URLField(
max_length=400, verbose_name=_('CMIS Atom endpoint'), max_length=400, verbose_name=_('CMIS Atom endpoint'), help_text=_('URL of the CMIS Atom endpoint')
help_text=_('URL of the CMIS Atom endpoint')) )
username = models.CharField(max_length=128, verbose_name=_('Service username')) username = models.CharField(max_length=128, verbose_name=_('Service username'))
password = models.CharField(max_length=128, verbose_name=_('Service password')) password = models.CharField(max_length=128, verbose_name=_('Service password'))
category = _('Business Process Connectors') category = _('Business Process Connectors')
@ -94,7 +91,8 @@ class CmisConnector(BaseResource):
'application/json': UPLOAD_SCHEMA, 'application/json': UPLOAD_SCHEMA,
} }
} }
}) },
)
def uploadfile(self, request, post_data): def uploadfile(self, request, post_data):
error, error_msg, data = self._validate_inputs(post_data) error, error_msg, data = self._validate_inputs(post_data)
if error: if error:
@ -114,7 +112,7 @@ class CmisConnector(BaseResource):
return {'data': {'properties': doc.properties}} return {'data': {'properties': doc.properties}}
def _validate_inputs(self, data): def _validate_inputs(self, data):
""" process dict """process dict
return a tuple (error, error_msg, data) return a tuple (error, error_msg, data)
""" """
file_ = data['file'] file_ = data['file']
@ -149,11 +147,11 @@ def wrap_cmis_error(f):
raise APIError("invalid property name: %s" % e) raise APIError("invalid property name: %s" % e)
except CmisException as e: except CmisException as e:
raise APIError("cmis binding error: %s" % e) raise APIError("cmis binding error: %s" % e)
return wrapper return wrapper
class CMISGateway(object): class CMISGateway(object):
def __init__(self, cmis_endpoint, username, password, logger): def __init__(self, cmis_endpoint, username, password, logger):
self._cmis_client = CmisClient(cmis_endpoint, username, password) self._cmis_client = CmisClient(cmis_endpoint, username, password)
self._logger = logger self._logger = logger
@ -182,11 +180,13 @@ class CMISGateway(object):
return folder return folder
@wrap_cmis_error @wrap_cmis_error
def create_doc(self, file_name, file_path, file_byte_content, def create_doc(
content_type=None, object_type=None, properties=None): self, file_name, file_path, file_byte_content, content_type=None, object_type=None, properties=None
):
folder = self._get_or_create_folder(file_path) folder = self._get_or_create_folder(file_path)
properties = properties or {} properties = properties or {}
if object_type: if object_type:
properties['cmis:objectTypeId'] = object_type properties['cmis:objectTypeId'] = object_type
return folder.createDocument(file_name, contentFile=BytesIO(file_byte_content), return folder.createDocument(
contentType=content_type, properties=properties) file_name, contentFile=BytesIO(file_byte_content), contentType=content_type, properties=properties
)

View File

@ -19,6 +19,5 @@ from django.conf.urls import include, url
from .views import CmisTypeView from .views import CmisTypeView
management_urlpatterns = [ management_urlpatterns = [
url(r'^(?P<connector_slug>[\w,-]+)/type/$', url(r'^(?P<connector_slug>[\w,-]+)/type/$', CmisTypeView.as_view(), name='cmis-type'),
CmisTypeView.as_view(), name='cmis-type'),
] ]

View File

@ -30,8 +30,7 @@ class CmisTypeView(TemplateView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.connector = CmisConnector.objects.get(slug=kwargs['connector_slug']) self.connector = CmisConnector.objects.get(slug=kwargs['connector_slug'])
client = CmisClient(self.connector.cmis_endpoint, self.connector.username, client = CmisClient(self.connector.cmis_endpoint, self.connector.username, self.connector.password)
self.connector.password)
self.repo = client.getDefaultRepository() self.repo = client.getDefaultRepository()
type_id = request.GET.get('id') type_id = request.GET.get('id')

View File

@ -20,7 +20,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CryptedFile', name='CryptedFile',
fields=[ fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
'uuid',
models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False),
),
('filename', models.CharField(max_length=512)), ('filename', models.CharField(max_length=512)),
('content_type', models.CharField(max_length=128)), ('content_type', models.CharField(max_length=128)),
('creation_timestamp', models.DateTimeField(auto_now_add=True)), ('creation_timestamp', models.DateTimeField(auto_now_add=True)),
@ -29,14 +32,44 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Cryptor', name='Cryptor',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')), ('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(unique=True, verbose_name='Identifier')), ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('public_key', models.TextField(blank=True, validators=[passerelle.apps.cryptor.models.validate_rsa_key], verbose_name='Encryption RSA public key (PEM format)')), (
('private_key', models.TextField(blank=True, validators=[passerelle.apps.cryptor.models.validate_rsa_key], verbose_name='Decryption RSA private key (PEM format)')), 'public_key',
('redirect_url_base', models.URLField(blank=True, help_text='Base URL for redirect, empty for local', max_length=256, verbose_name='Base URL of decryption system')), models.TextField(
('users', models.ManyToManyField(blank=True, related_name='_cryptor_users_+', related_query_name='+', to='base.ApiUser')), blank=True,
validators=[passerelle.apps.cryptor.models.validate_rsa_key],
verbose_name='Encryption RSA public key (PEM format)',
),
),
(
'private_key',
models.TextField(
blank=True,
validators=[passerelle.apps.cryptor.models.validate_rsa_key],
verbose_name='Decryption RSA private key (PEM format)',
),
),
(
'redirect_url_base',
models.URLField(
blank=True,
help_text='Base URL for redirect, empty for local',
max_length=256,
verbose_name='Base URL of decryption system',
),
),
(
'users',
models.ManyToManyField(
blank=True, related_name='_cryptor_users_+', related_query_name='+', to='base.ApiUser'
),
),
], ],
options={ options={
'verbose_name': 'Encryption / Decryption', 'verbose_name': 'Encryption / Decryption',

View File

@ -52,15 +52,16 @@ FILE_SCHEMA = {
"filename": {"type": "string"}, "filename": {"type": "string"},
"content_type": {"type": "string"}, "content_type": {"type": "string"},
"content": {"type": "string"}, "content": {"type": "string"},
} },
} }
} },
} }
# encrypt and decrypt are borrowed from # encrypt and decrypt are borrowed from
# https://www.pycryptodome.org/en/latest/src/examples.html#encrypt-data-with-rsa # https://www.pycryptodome.org/en/latest/src/examples.html#encrypt-data-with-rsa
def write_encrypt(out_file, data, key_pem): def write_encrypt(out_file, data, key_pem):
public_key = RSA.import_key(key_pem) public_key = RSA.import_key(key_pem)
session_key = get_random_bytes(16) session_key = get_random_bytes(16)
@ -115,15 +116,18 @@ def validate_rsa_key(key):
class Cryptor(BaseResource): class Cryptor(BaseResource):
public_key = models.TextField(blank=True, public_key = models.TextField(
verbose_name=_('Encryption RSA public key (PEM format)'), blank=True, verbose_name=_('Encryption RSA public key (PEM format)'), validators=[validate_rsa_key]
validators=[validate_rsa_key]) )
private_key = models.TextField(blank=True, private_key = models.TextField(
verbose_name=_('Decryption RSA private key (PEM format)'), blank=True, verbose_name=_('Decryption RSA private key (PEM format)'), validators=[validate_rsa_key]
validators=[validate_rsa_key]) )
redirect_url_base = models.URLField(max_length=256, blank=True, redirect_url_base = models.URLField(
verbose_name=_('Base URL of decryption system'), max_length=256,
help_text=_('Base URL for redirect, empty for local')) blank=True,
verbose_name=_('Base URL of decryption system'),
help_text=_('Base URL for redirect, empty for local'),
)
category = _('Misc') category = _('Misc')
@ -136,20 +140,23 @@ class Cryptor(BaseResource):
return _('this file-decrypt endpoint') return _('this file-decrypt endpoint')
def get_filename(self, uuid, create=False): def get_filename(self, uuid, create=False):
dirname = os.path.join(default_storage.path(self.get_connector_slug()), dirname = os.path.join(
self.slug, uuid[0:2], uuid[2:4]) default_storage.path(self.get_connector_slug()), self.slug, uuid[0:2], uuid[2:4]
)
if create: if create:
makedir(dirname) makedir(dirname)
filename = os.path.join(dirname, uuid) filename = os.path.join(dirname, uuid)
return filename return filename
@endpoint(
@endpoint(name='file-encrypt', perm='can_encrypt', name='file-encrypt',
description=_('Encrypt a file'), perm='can_encrypt',
post={ description=_('Encrypt a file'),
'description': _('File to encrypt'), post={
'request_body': {'schema': {'application/json': FILE_SCHEMA}} 'description': _('File to encrypt'),
}) 'request_body': {'schema': {'application/json': FILE_SCHEMA}},
},
)
def file_encrypt(self, request, post_data): def file_encrypt(self, request, post_data):
if not self.public_key: if not self.public_key:
raise APIError('missing public key') raise APIError('missing public key')
@ -168,8 +175,7 @@ class Cryptor(BaseResource):
if self.redirect_url_base: if self.redirect_url_base:
redirect_url_base = self.redirect_url_base redirect_url_base = self.redirect_url_base
else: else:
redirect_url_base = request.build_absolute_uri('%sfile-decrypt/' % ( redirect_url_base = request.build_absolute_uri('%sfile-decrypt/' % (self.get_absolute_url(),))
self.get_absolute_url(),))
redirect_url = urljoin(redirect_url_base, uuid) redirect_url = urljoin(redirect_url_base, uuid)
content_filename = self.get_filename(uuid, create=True) content_filename = self.get_filename(uuid, create=True)
@ -189,16 +195,19 @@ class Cryptor(BaseResource):
return {'data': metadata} return {'data': metadata}
@endpoint(name='file-decrypt', perm='can_decrypt', @endpoint(
description=_('Decrypt a file'), name='file-decrypt',
pattern=r'(?P<uuid>[\w-]+)$', perm='can_decrypt',
example_pattern='{uuid}/', description=_('Decrypt a file'),
parameters={ pattern=r'(?P<uuid>[\w-]+)$',
'uuid': { example_pattern='{uuid}/',
'description': _('File identifier'), parameters={
'example_value': '12345678-abcd-4321-abcd-123456789012', 'uuid': {
}, 'description': _('File identifier'),
}) 'example_value': '12345678-abcd-4321-abcd-123456789012',
},
},
)
def file_decrypt(self, request, uuid): def file_decrypt(self, request, uuid):
if not self.private_key: if not self.private_key:
raise APIError('missing private key') raise APIError('missing private key')

View File

@ -16,12 +16,15 @@
import django.apps import django.apps
class AppConfig(django.apps.AppConfig): class AppConfig(django.apps.AppConfig):
name = 'passerelle.apps.csvdatasource' name = 'passerelle.apps.csvdatasource'
label = 'csvdatasource' label = 'csvdatasource'
def get_connector_model(self): def get_connector_model(self):
from . import models from . import models
return models.CsvDataSource return models.CsvDataSource
default_app_config = 'passerelle.apps.csvdatasource.AppConfig' default_app_config = 'passerelle.apps.csvdatasource.AppConfig'

View File

@ -41,25 +41,38 @@ class QueryForm(forms.ModelForm):
if named: if named:
line = line.split(':', 1) line = line.split(':', 1)
if len(line) != 2: if len(line) != 2:
errors.append(ValidationError( errors.append(
_('Syntax error line %d: each line must be prefixed ' ValidationError(
'with an identifier followed by a colon.') % (i + 1))) _(
'Syntax error line %d: each line must be prefixed '
'with an identifier followed by a colon.'
)
% (i + 1)
)
)
continue continue
name, line = line name, line = line
if not identifier_re.match(name): if not identifier_re.match(name):
errors.append( errors.append(
ValidationError(_('Syntax error line %d: invalid identifier, ' ValidationError(
'it must starts with a letter and only ' _(
'contains letters, digits and _.') % (i + 1))) 'Syntax error line %d: invalid identifier, '
'it must starts with a letter and only '
'contains letters, digits and _.'
)
% (i + 1)
)
)
continue continue
try: try:
get_code(line) get_code(line)
except SyntaxError as e: except SyntaxError as e:
errors.append(ValidationError( errors.append(
_('Syntax error line %(line)d at character %(character)d') % { ValidationError(
'line': i + 1, _('Syntax error line %(line)d at character %(character)d')
'character': e.offset % {'line': i + 1, 'character': e.offset}
})) )
)
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
return lines return lines

View File

@ -14,19 +14,41 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='CsvDataSource', name='CsvDataSource',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('title', models.CharField(verbose_name='Title', max_length=50)), ('title', models.CharField(verbose_name='Title', max_length=50)),
('slug', models.SlugField(verbose_name='Identifier', unique=True)), ('slug', models.SlugField(verbose_name='Identifier', unique=True)),
('description', models.TextField(verbose_name='Description')), ('description', models.TextField(verbose_name='Description')),
('csv_file', models.FileField(help_text='Supported file formats: csv, ods, xls, xlsx', (
upload_to=b'csv', verbose_name='Spreadsheet file')), 'csv_file',
('columns_keynames', models.CharField(default=b'id, text', models.FileField(
help_text='ex: id,text,data1,data2', help_text='Supported file formats: csv, ods, xls, xlsx',
max_length=256, upload_to=b'csv',
verbose_name='Column keynames', verbose_name='Spreadsheet file',
blank=True)), ),
),
(
'columns_keynames',
models.CharField(
default=b'id, text',
help_text='ex: id,text,data1,data2',
max_length=256,
verbose_name='Column keynames',
blank=True,
),
),
('skip_header', models.BooleanField(default=False, verbose_name='Skip first line')), ('skip_header', models.BooleanField(default=False, verbose_name='Skip first line')),
('users', models.ManyToManyField(to='base.ApiUser', related_name='_csvdatasource_users_+', related_query_name='+', blank=True)), (
'users',
models.ManyToManyField(
to='base.ApiUser',
related_name='_csvdatasource_users_+',
related_query_name='+',
blank=True,
),
),
], ],
options={ options={
'verbose_name': 'CSV File', 'verbose_name': 'CSV File',

View File

@ -14,7 +14,13 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='csvdatasource', model_name='csvdatasource',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Debug Enabled', blank=True, choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Debug Enabled',
blank=True,
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,7 +14,12 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='csvdatasource', model_name='csvdatasource',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[(b'DEBUG', b'DEBUG'), (b'INFO', b'INFO')],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,7 +14,19 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='csvdatasource', model_name='csvdatasource',
name='log_level', name='log_level',
field=models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL')]), field=models.CharField(
default=b'NOTSET',
max_length=10,
verbose_name='Log Level',
choices=[
(b'NOTSET', b'NOTSET'),
(b'DEBUG', b'DEBUG'),
(b'INFO', b'INFO'),
(b'WARNING', b'WARNING'),
(b'ERROR', b'ERROR'),
(b'CRITICAL', b'CRITICAL'),
],
),
preserve_default=True, preserve_default=True,
), ),
] ]

View File

@ -14,13 +14,54 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Query', name='Query',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('slug', models.SlugField(verbose_name='Name')), ('slug', models.SlugField(verbose_name='Name')),
('filters', models.TextField(help_text='List of filter clauses (Python expression)', verbose_name='Filters', blank=True)), (
('projections', models.TextField(help_text='List of projections (name:expression)', verbose_name='Projections', blank=True)), 'filters',
('order', models.TextField(help_text='Columns to use for sorting rows', verbose_name='Sort Order', blank=True)), models.TextField(
('distinct', models.TextField(help_text='Distinct columns', verbose_name='Distinct', blank=True)), help_text='List of filter clauses (Python expression)',
('structure', models.CharField(choices=[(b'array', 'Array'), (b'dict', 'Dictionary'), (b'keyed-distinct', 'Keyed Dictionary'), (b'tuples', 'Tuples'), (b'onerow', 'Single Row'), (b'one', 'Single Value')], default=b'dict', help_text='Data structure used for the response', max_length=20, verbose_name='Structure')), verbose_name='Filters',
blank=True,
),
),
(
'projections',
models.TextField(
help_text='List of projections (name:expression)',
verbose_name='Projections',
blank=True,
),
),
(
'order',
models.TextField(
help_text='Columns to use for sorting rows', verbose_name='Sort Order', blank=True
),
),
(
'distinct',
models.TextField(help_text='Distinct columns', verbose_name='Distinct', blank=True),
),
(
'structure',
models.CharField(
choices=[
(b'array', 'Array'),
(b'dict', 'Dictionary'),
(b'keyed-distinct', 'Keyed Dictionary'),
(b'tuples', 'Tuples'),
(b'onerow', 'Single Row'),
(b'one', 'Single Value'),
],
default=b'dict',
help_text='Data structure used for the response',
max_length=20,
verbose_name='Structure',
),
),
('resource', models.ForeignKey(to='csvdatasource.CsvDataSource', on_delete=models.CASCADE)), ('resource', models.ForeignKey(to='csvdatasource.CsvDataSource', on_delete=models.CASCADE)),
], ],
options={ options={

View File

@ -17,7 +17,10 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='TableRow', name='TableRow',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('line_number', models.IntegerField()), ('line_number', models.IntegerField()),
('data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)), ('data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
], ],
@ -28,6 +31,8 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='tablerow', model_name='tablerow',
name='resource', name='resource',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='csvdatasource.CsvDataSource'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to='csvdatasource.CsvDataSource'
),
), ),
] ]

View File

@ -8,7 +8,9 @@ def generate_slug(instance):
slug = instance.slug slug = instance.slug
i = 1 i = 1
while True: while True:
queryset = instance._meta.model.objects.filter(slug=slug, resource=instance.resource).exclude(pk=instance.pk) queryset = instance._meta.model.objects.filter(slug=slug, resource=instance.resource).exclude(
pk=instance.pk
)
if not queryset.exists(): if not queryset.exists():
break break
slug = '%s-%s' % (instance.slug, i) slug = '%s-%s' % (instance.slug, i)

View File

@ -16,21 +16,48 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='csvdatasource', model_name='csvdatasource',
name='columns_keynames', name='columns_keynames',
field=models.CharField(blank=True, default='id, text', help_text='ex: id,text,data1,data2', max_length=256, verbose_name='Column keynames'), field=models.CharField(
blank=True,
default='id, text',
help_text='ex: id,text,data1,data2',
max_length=256,
verbose_name='Column keynames',
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='csvdatasource', model_name='csvdatasource',
name='csv_file', name='csv_file',
field=models.FileField(help_text='Supported file formats: csv, ods, xls, xlsx', upload_to='csv', verbose_name='Spreadsheet file'), field=models.FileField(
help_text='Supported file formats: csv, ods, xls, xlsx',
upload_to='csv',
verbose_name='Spreadsheet file',
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='query', model_name='query',
name='resource', name='resource',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='queries', to='csvdatasource.CsvDataSource'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='queries',
to='csvdatasource.CsvDataSource',
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='query', model_name='query',
name='structure', name='structure',
field=models.CharField(choices=[('array', 'Array'), ('dict', 'Dictionary'), ('keyed-distinct', 'Keyed Dictionary'), ('tuples', 'Tuples'), ('onerow', 'Single Row'), ('one', 'Single Value')], default='dict', help_text='Data structure used for the response', max_length=20, verbose_name='Structure'), field=models.CharField(
choices=[
('array', 'Array'),
('dict', 'Dictionary'),
('keyed-distinct', 'Keyed Dictionary'),
('tuples', 'Tuples'),
('onerow', 'Single Row'),
('one', 'Single Value'),
],
default='dict',
help_text='Data structure used for the response',
max_length=20,
verbose_name='Structure',
),
), ),
] ]

View File

@ -15,6 +15,10 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='csvdatasource', model_name='csvdatasource',
name='csv_file', name='csv_file',
field=models.FileField(help_text='Supported file formats: csv, ods, xls, xlsx', upload_to=passerelle.apps.csvdatasource.models.upload_to, verbose_name='Spreadsheet file'), field=models.FileField(
help_text='Supported file formats: csv, ods, xls, xlsx',
upload_to=passerelle.apps.csvdatasource.models.upload_to,
verbose_name='Spreadsheet file',
),
), ),
] ]

View File

@ -52,7 +52,7 @@ code_cache = OrderedDict()
def get_code(expr): def get_code(expr):
# limit size of code cache to 1024 # limit size of code cache to 1024
if len(code_cache) > 1024: if len(code_cache) > 1024:
for key in list(code_cache.keys())[:len(code_cache) - 1024]: for key in list(code_cache.keys())[: len(code_cache) - 1024]:
code_cache.pop(key) code_cache.pop(key)
if expr not in code_cache: if expr not in code_cache:
code_cache[expr] = compile(expr, '<inline>', 'eval') code_cache[expr] = compile(expr, '<inline>', 'eval')
@ -65,21 +65,13 @@ class Query(models.Model):
label = models.CharField(_('Label'), max_length=100) label = models.CharField(_('Label'), max_length=100)
description = models.TextField(_('Description'), blank=True) description = models.TextField(_('Description'), blank=True)
filters = models.TextField( filters = models.TextField(
_('Filters'), _('Filters'), blank=True, help_text=_('List of filter clauses (Python expression)')
blank=True, )
help_text=_('List of filter clauses (Python expression)')) order = models.TextField(_('Sort Order'), blank=True, help_text=_('Columns to use for sorting rows'))
order = models.TextField( distinct = models.TextField(_('Distinct'), blank=True, help_text=_('Distinct columns'))
_('Sort Order'),
blank=True,
help_text=_('Columns to use for sorting rows'))
distinct = models.TextField(
_('Distinct'),
blank=True,
help_text=_('Distinct columns'))
projections = models.TextField( projections = models.TextField(
_('Projections'), _('Projections'), blank=True, help_text=_('List of projections (name:expression)')
blank=True, )
help_text=_('List of projections (name:expression)'))
structure = models.CharField( structure = models.CharField(
_('Structure'), _('Structure'),
max_length=20, max_length=20,
@ -89,9 +81,11 @@ class Query(models.Model):
('keyed-distinct', _('Keyed Dictionary')), ('keyed-distinct', _('Keyed Dictionary')),
('tuples', _('Tuples')), ('tuples', _('Tuples')),
('onerow', _('Single Row')), ('onerow', _('Single Row')),
('one', _('Single Value'))], ('one', _('Single Value')),
],
default='dict', default='dict',
help_text=_('Data structure used for the response')) help_text=_('Data structure used for the response'),
)
class Meta: class Meta:
ordering = ['slug'] ordering = ['slug']
@ -123,12 +117,10 @@ class Query(models.Model):
return self.slug return self.slug
def delete_url(self): def delete_url(self):
return reverse('csv-delete-query', return reverse('csv-delete-query', kwargs={'connector_slug': self.resource.slug, 'pk': self.pk})
kwargs={'connector_slug': self.resource.slug, 'pk': self.pk})
def edit_url(self): def edit_url(self):
return reverse('csv-edit-query', return reverse('csv-edit-query', kwargs={'connector_slug': self.resource.slug, 'pk': self.pk})
kwargs={'connector_slug': self.resource.slug, 'pk': self.pk})
def upload_to(instance, filename): def upload_to(instance, filename):
@ -137,20 +129,23 @@ def upload_to(instance, filename):
class CsvDataSource(BaseResource): class CsvDataSource(BaseResource):
csv_file = models.FileField( csv_file = models.FileField(
_('Spreadsheet file'), _('Spreadsheet file'), upload_to=upload_to, help_text=_('Supported file formats: csv, ods, xls, xlsx')
upload_to=upload_to, )
help_text=_('Supported file formats: csv, ods, xls, xlsx'))
columns_keynames = models.CharField( columns_keynames = models.CharField(
max_length=256, max_length=256,
verbose_name=_('Column keynames'), verbose_name=_('Column keynames'),
default='id, text', default='id, text',
help_text=_('ex: id,text,data1,data2'), blank=True) help_text=_('ex: id,text,data1,data2'),
blank=True,
)
skip_header = models.BooleanField(_('Skip first line'), default=False) skip_header = models.BooleanField(_('Skip first line'), default=False)
_dialect_options = JSONField(editable=False, null=True) _dialect_options = JSONField(editable=False, null=True)
sheet_name = models.CharField(_('Sheet name'), blank=True, max_length=150) sheet_name = models.CharField(_('Sheet name'), blank=True, max_length=150)
category = _('Data Sources') category = _('Data Sources')
documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/parametrage-avance/source-de-donnees-csv/' documentation_url = (
'https://doc-publik.entrouvert.com/admin-fonctionnel/parametrage-avance/source-de-donnees-csv/'
)
class Meta: class Meta:
verbose_name = _('Spreadsheet File') verbose_name = _('Spreadsheet File')
@ -173,9 +168,7 @@ class CsvDataSource(BaseResource):
def _detect_dialect_options(self): def _detect_dialect_options(self):
content = self.get_content_without_bom() content = self.get_content_without_bom()
dialect = csv.Sniffer().sniff(content) dialect = csv.Sniffer().sniff(content)
self.dialect_options = { self.dialect_options = {k: v for k, v in vars(dialect).items() if not k.startswith('_')}
k: v for k, v in vars(dialect).items() if not k.startswith('_')
}
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
cache = kwargs.pop('cache', True) cache = kwargs.pop('cache', True)
@ -193,7 +186,8 @@ class CsvDataSource(BaseResource):
TableRow.objects.filter(resource=self).delete() TableRow.objects.filter(resource=self).delete()
for block in batch(enumerate(self.get_rows()), 5000): for block in batch(enumerate(self.get_rows()), 5000):
TableRow.objects.bulk_create( TableRow.objects.bulk_create(
TableRow(resource=self, line_number=i, data=data) for i, data in block) TableRow(resource=self, line_number=i, data=data) for i, data in block
)
def csv_file_datetime(self): def csv_file_datetime(self):
ctime = os.fstat(self.csv_file.fileno()).st_ctime ctime = os.fstat(self.csv_file.fileno()).st_ctime
@ -205,8 +199,7 @@ class CsvDataSource(BaseResource):
@property @property
def dialect_options(self): def dialect_options(self):
"""turn dict items into string """turn dict items into string"""
"""
file_type = self.csv_file.name.split('.')[-1] file_type = self.csv_file.name.split('.')[-1]
if file_type in ('ods', 'xls', 'xlsx'): if file_type in ('ods', 'xls', 'xlsx'):
return None return None
@ -317,8 +310,7 @@ class CsvDataSource(BaseResource):
query = Query(filters='\n'.join(filters)) query = Query(filters='\n'.join(filters))
return self.execute_query(request, query, query_params=params.dict()) return self.execute_query(request, query, query_params=params.dict())
@endpoint(perm='can_access', methods=['get'], @endpoint(perm='can_access', methods=['get'], name='query', pattern=r'^(?P<query_name>[\w-]+)/$')
name='query', pattern=r'^(?P<query_name>[\w-]+)/$')
def select(self, request, query_name, **kwargs): def select(self, request, query_name, **kwargs):
try: try:
query = Query.objects.get(resource=self.id, slug=query_name) query = Query.objects.get(resource=self.id, slug=query_name)
@ -371,8 +363,7 @@ class CsvDataSource(BaseResource):
filters = query.get_list('filters') filters = query.get_list('filters')
if filters: if filters:
data = [row for new_row, row in stream_expressions(filters, data, kind='filters') data = [row for new_row, row in stream_expressions(filters, data, kind='filters') if all(new_row)]
if all(new_row)]
order = query.get_list('order') order = query.get_list('order')
if order: if order:
@ -391,11 +382,13 @@ class CsvDataSource(BaseResource):
try: try:
hash(new_row) hash(new_row)
except TypeError: except TypeError:
raise APIError(u'distinct value is unhashable', raise APIError(
data={ u'distinct value is unhashable',
'row': repr(row), data={
'distinct': repr(new_row), 'row': repr(row),
}) 'distinct': repr(new_row),
},
)
if new_row in seen: if new_row in seen:
continue continue
new_data.append(row) new_data.append(row)
@ -413,24 +406,21 @@ class CsvDataSource(BaseResource):
titles.append(name) titles.append(name)
expressions.append(expr) expressions.append(expr)
new_data = [] new_data = []
for new_row, row in stream_expressions(expressions, data, kind='projection', for new_row, row in stream_expressions(expressions, data, kind='projection', titles=titles):
titles=titles):
new_data.append(dict(zip(titles, new_row))) new_data.append(dict(zip(titles, new_row)))
data = new_data data = new_data
if 'id' in request.GET: if 'id' in request.GET:
# always provide a ?id= filter. # always provide a ?id= filter.
filters = ["id == %r" % force_text(request.GET['id'])] filters = ["id == %r" % force_text(request.GET['id'])]
data = [row for new_row, row in stream_expressions(filters, data, kind='filters') data = [row for new_row, row in stream_expressions(filters, data, kind='filters') if new_row[0]]
if new_row[0]]
# allow jsonp queries by select2 # allow jsonp queries by select2
# filtering is done there after projection because we need a projection named text for # filtering is done there after projection because we need a projection named text for
# retro-compatibility with previous use of the csvdatasource with select2 # retro-compatibility with previous use of the csvdatasource with select2
if 'q' in request.GET: if 'q' in request.GET:
filters = ["%s in normalize(text.lower())" % repr(normalize(request.GET['q'].lower()))] filters = ["%s in normalize(text.lower())" % repr(normalize(request.GET['q'].lower()))]
data = [row for new_row, row in stream_expressions(filters, data, kind='filters') data = [row for new_row, row in stream_expressions(filters, data, kind='filters') if new_row[0]]
if new_row[0]]
# force rendition of iterator as list # force rendition of iterator as list
data = list(data) data = list(data)
@ -450,7 +440,7 @@ class CsvDataSource(BaseResource):
raise APIError('invalid offset parameter') raise APIError('invalid offset parameter')
# paginate data # paginate data
data = data[offset:offset+limit] data = data[offset : offset + limit]
if query.structure == 'array': if query.structure == 'array':
return {'data': [[row[t] for t in titles] for row in data]} return {'data': [[row[t] for t in titles] for row in data]}
@ -539,4 +529,4 @@ class TableRow(models.Model):
class Meta: class Meta:
ordering = ('line_number',) ordering = ('line_number',)
unique_together = (('resource', 'line_number')) unique_together = ('resource', 'line_number')

View File

@ -19,12 +19,16 @@ from django.conf.urls import include, url
from .views import * from .views import *
management_urlpatterns = [ management_urlpatterns = [
url(r'^(?P<connector_slug>[\w,-]+)/download/$', url(r'^(?P<connector_slug>[\w,-]+)/download/$', CsvDownload.as_view(), name='csv-download'),
CsvDownload.as_view(), name='csv-download'), url(r'^(?P<connector_slug>[\w,-]+)/queries/new/$', NewQueryView.as_view(), name='csv-new-query'),
url(r'^(?P<connector_slug>[\w,-]+)/queries/new/$', url(
NewQueryView.as_view(), name='csv-new-query'), r'^(?P<connector_slug>[\w,-]+)/queries/(?P<pk>[\w,-]+)/$',
url(r'^(?P<connector_slug>[\w,-]+)/queries/(?P<pk>[\w,-]+)/$', UpdateQueryView.as_view(),
UpdateQueryView.as_view(), name='csv-edit-query'), name='csv-edit-query',
url(r'^(?P<connector_slug>[\w,-]+)/queries/(?P<pk>[\w,-]+)/delete$', ),
DeleteQueryView.as_view(), name='csv-delete-query'), url(
r'^(?P<connector_slug>[\w,-]+)/queries/(?P<pk>[\w,-]+)/delete$',
DeleteQueryView.as_view(),
name='csv-delete-query',
),
] ]

View File

@ -29,9 +29,11 @@ from django.utils.translation import ugettext_lazy as _
from ..models import Invoice from ..models import Invoice
def u(s): def u(s):
return force_text(s, 'iso-8859-15') return force_text(s, 'iso-8859-15')
class Loader(object): class Loader(object):
def __init__(self, connector): def __init__(self, connector):
self.connector = connector self.connector = connector
@ -45,6 +47,7 @@ class Loader(object):
fd = archive.open('data_full.csv') fd = archive.open('data_full.csv')
if six.PY3: if six.PY3:
import io import io
fd = io.TextIOWrapper(fd, 'iso-8859-15') fd = io.TextIOWrapper(fd, 'iso-8859-15')
csvfile = six.StringIO(fd.read()) csvfile = six.StringIO(fd.read())
csvreader = csv.reader(csvfile, delimiter='\t') csvreader = csv.reader(csvfile, delimiter='\t')
@ -59,9 +62,11 @@ class Loader(object):
invoice['amount'] = str(Decimal(invoice['total_amount']) - paid_amount) invoice['amount'] = str(Decimal(invoice['total_amount']) - paid_amount)
invoice['paid'] = bool(Decimal(invoice['amount']) == 0) invoice['paid'] = bool(Decimal(invoice['amount']) == 0)
invoice['issue_date'] = datetime.datetime.strptime( invoice['issue_date'] = datetime.datetime.strptime(
row['DAT_GENERATION_FAC'], '%d/%m/%Y').strftime('%Y-%m-%d') row['DAT_GENERATION_FAC'], '%d/%m/%Y'
).strftime('%Y-%m-%d')
invoice['pay_limit_date'] = datetime.datetime.strptime( invoice['pay_limit_date'] = datetime.datetime.strptime(
row['DAT_LIMITEPAIE_FAC'], '%d/%m/%Y').strftime('%Y-%m-%d') row['DAT_LIMITEPAIE_FAC'], '%d/%m/%Y'
).strftime('%Y-%m-%d')
invoice['online_payment'] = True invoice['online_payment'] = True
invoice['no_online_payment_reason'] = None invoice['no_online_payment_reason'] = None
if not invoice['paid']: if not invoice['paid']:
@ -73,10 +78,12 @@ class Loader(object):
invoice['online_payment'] = False invoice['online_payment'] = False
invoice['no_online_payment_reason'] = 'autobilling' invoice['no_online_payment_reason'] = 'autobilling'
obj, created = Invoice.objects.update_or_create(resource=self.connector, obj, created = Invoice.objects.update_or_create(
external_id=row['ID_FAC'], defaults=invoice) resource=self.connector, external_id=row['ID_FAC'], defaults=invoice
)
invoice_filename = '%s_%s.pdf' % ( invoice_filename = '%s_%s.pdf' % (
datetime.datetime.strptime(row['DAT_DEBUT_PGE'], '%d/%m/%Y').strftime('%Y-%m'), datetime.datetime.strptime(row['DAT_DEBUT_PGE'], '%d/%m/%Y').strftime('%Y-%m'),
row['ID_FAC']) row['ID_FAC'],
)
if invoice_filename in archive_files: if invoice_filename in archive_files:
obj.write_pdf(archive.read(invoice_filename)) obj.write_pdf(archive.read(invoice_filename))

View File

@ -52,7 +52,9 @@ def normalize_adult(adult):
def normalize_family(family, adults): def normalize_family(family, adults):
return { return {
'external_id': family['id_fam'], 'external_id': family['id_fam'],
'adults': [adults[family[id]] for id in ('id_per1', 'id_per2') if family[id] and adults.get(family[id])], 'adults': [
adults[family[id]] for id in ('id_per1', 'id_per2') if family[id] and adults.get(family[id])
],
'children': [], 'children': [],
'invoices': [], 'invoices': [],
'login': family['id_fam'], 'login': family['id_fam'],
@ -65,6 +67,7 @@ def normalize_family(family, adults):
'city': family['lib_commune_adr'], 'city': family['lib_commune_adr'],
} }
def normalize_child(child): def normalize_child(child):
sex = child['typ_sexe_per'] sex = child['typ_sexe_per']
if sex == 'G': if sex == 'G':
@ -74,27 +77,30 @@ def normalize_child(child):
'first_name': child['lib_prenom_per'], 'first_name': child['lib_prenom_per'],
'last_name': child['lib_nom_per'], 'last_name': child['lib_nom_per'],
'sex': sex, 'sex': sex,
'birthdate': get_date(child['dat_naissance']) 'birthdate': get_date(child['dat_naissance']),
} }
def normalize_invoice(i): def normalize_invoice(i):
invoice = {'external_id': i['id_fac'], invoice = {
'label': i['id_fac'], 'external_id': i['id_fac'],
'total_amount': Decimal(i['mnt_facture_fac']), 'label': i['id_fac'],
'amount': Decimal(i['mnt_solde_fac']), 'total_amount': Decimal(i['mnt_facture_fac']),
'issue_date': i['dat_generation_fac'], 'amount': Decimal(i['mnt_solde_fac']),
'pay_limit_date': get_date(i['dat_limitepaie_fac']), 'issue_date': i['dat_generation_fac'],
'autobilling': i['on_prelevauto_ins'] == 'O', 'pay_limit_date': get_date(i['dat_limitepaie_fac']),
'online_payment': True, 'autobilling': i['on_prelevauto_ins'] == 'O',
'payment_date': get_datetime(i['dat_reglement']), 'online_payment': True,
'litigation_date': get_date(i['dat_perception_fac']), 'payment_date': get_datetime(i['dat_reglement']),
'paid': Decimal(i['mnt_solde_fac']) == 0 'litigation_date': get_date(i['dat_perception_fac']),
'paid': Decimal(i['mnt_solde_fac']) == 0,
} }
return invoice return invoice
class Dialect(csv.Dialect): class Dialect(csv.Dialect):
'''Because sometimes it cannot be sniffed by csv.Sniffer''' '''Because sometimes it cannot be sniffed by csv.Sniffer'''
delimiter = ';' delimiter = ';'
doublequote = False doublequote = False
escapechar = None escapechar = None
@ -104,13 +110,16 @@ class Dialect(csv.Dialect):
class Loader(object): class Loader(object):
def __init__(self, connector): def __init__(self, connector):
self.connector = connector self.connector = connector
def clean(self, archive): def clean(self, archive):
for filename in ('extract_prcit_personne.csv', 'extract_prcit_famille.csv', for filename in (
'extract_prcit_enfant.csv', 'extract_prcit_facture.csv'): 'extract_prcit_personne.csv',
'extract_prcit_famille.csv',
'extract_prcit_enfant.csv',
'extract_prcit_facture.csv',
):
if not filename in archive.namelist(): if not filename in archive.namelist():
raise ValidationError(_('Missing %(filename)s file in zip.') % {'filename': filename}) raise ValidationError(_('Missing %(filename)s file in zip.') % {'filename': filename})
@ -118,6 +127,7 @@ class Loader(object):
fd = self.archive.open(filename) fd = self.archive.open(filename)
if six.PY3: if six.PY3:
import io import io
fd = io.TextIOWrapper(fd, 'iso-8859-15') fd = io.TextIOWrapper(fd, 'iso-8859-15')
reader = csv.reader(fd, Dialect) reader = csv.reader(fd, Dialect)
@ -144,7 +154,6 @@ class Loader(object):
families[invoice['id_fam']]['invoices'].append(normalize_invoice(invoice)) families[invoice['id_fam']]['invoices'].append(normalize_invoice(invoice))
return families return families
def load(self, archive): def load(self, archive):
self.archive = archive self.archive = archive
@ -157,20 +166,32 @@ class Loader(object):
import_start_timestamp = timezone.now() import_start_timestamp = timezone.now()
try: try:
for family_data in families.values(): for family_data in families.values():
data = dict_cherry_pick(family_data, data = dict_cherry_pick(
('login', 'password', 'family_quotient', family_data,
'zipcode', 'street_number', 'street_name', (
'address_complement', 'city')) 'login',
family, created = Family.objects.update_or_create(external_id=family_data['external_id'], 'password',
resource=self.connector, defaults=data) 'family_quotient',
'zipcode',
'street_number',
'street_name',
'address_complement',
'city',
),
)
family, created = Family.objects.update_or_create(
external_id=family_data['external_id'], resource=self.connector, defaults=data
)
for adult_data in family_data.get('adults') or []: for adult_data in family_data.get('adults') or []:
Adult.objects.update_or_create(family=family, Adult.objects.update_or_create(
external_id=adult_data['external_id'], defaults=adult_data) family=family, external_id=adult_data['external_id'], defaults=adult_data
)
for child_data in family_data.get('children') or []: for child_data in family_data.get('children') or []:
Child.objects.get_or_create(family=family, Child.objects.get_or_create(
external_id=child_data['external_id'], defaults=child_data) family=family, external_id=child_data['external_id'], defaults=child_data
)
for invoice_data in family_data.get('invoices') or []: for invoice_data in family_data.get('invoices') or []:
storage = DefaultStorage() storage = DefaultStorage()
@ -179,17 +200,27 @@ class Loader(object):
invoice_path = os.path.join(invoices_dir, invoice_filename) invoice_path = os.path.join(invoices_dir, invoice_filename)
# create invoice object only if associated pdf exists # create invoice object only if associated pdf exists
if os.path.exists(invoice_path): if os.path.exists(invoice_path):
invoice, created = Invoice.objects.update_or_create(resource=self.connector, invoice, created = Invoice.objects.update_or_create(
family=family, external_id=invoice_data['external_id'], defaults=invoice_data) resource=self.connector,
family=family,
external_id=invoice_data['external_id'],
defaults=invoice_data,
)
except Exception as e: except Exception as e:
self.connector.logger.error('Error occured while importing data: %s', e) self.connector.logger.error('Error occured while importing data: %s', e)
Family.objects.filter(resource=self.connector, update_timestamp__lte=import_start_timestamp).delete() Family.objects.filter(resource=self.connector, update_timestamp__lte=import_start_timestamp).delete()
Adult.objects.filter(family__resource=self.connector, update_timestamp__lte=import_start_timestamp).delete() Adult.objects.filter(
Child.objects.filter(family__resource=self.connector, update_timestamp__lte=import_start_timestamp).delete() family__resource=self.connector, update_timestamp__lte=import_start_timestamp
).delete()
Child.objects.filter(
family__resource=self.connector, update_timestamp__lte=import_start_timestamp
).delete()
# remove obsolete invoices and their pdfs # remove obsolete invoices and their pdfs
for invoice in Invoice.objects.filter(resource=self.connector, update_timestamp__lte=import_start_timestamp): for invoice in Invoice.objects.filter(
resource=self.connector, update_timestamp__lte=import_start_timestamp
):
if invoice.has_pdf: if invoice.has_pdf:
os.unlink(invoice.pdf_filename()) os.unlink(invoice.pdf_filename())
invoice.delete() invoice.delete()

Some files were not shown because too many files have changed in this diff Show More