mail: feed from MaarchCourrier (#22550)
This commit is contained in:
parent
fc5a4900ef
commit
a4ae8845e9
|
@ -11,7 +11,7 @@ Architecture: all
|
||||||
Depends: ${misc:Depends}, ${python:Depends},
|
Depends: ${misc:Depends}, ${python:Depends},
|
||||||
python-django (>= 1.7),
|
python-django (>= 1.7),
|
||||||
python-gadjo,
|
python-gadjo,
|
||||||
python-requests,
|
python-requests (>= 2.11),
|
||||||
python-django-haystack (>= 2.4.0),
|
python-django-haystack (>= 2.4.0),
|
||||||
python-django-reversion (>= 2.0.12),
|
python-django-reversion (>= 2.0.12),
|
||||||
python-django-taggit (>= 0.17.4),
|
python-django-taggit (>= 0.17.4),
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -106,6 +106,7 @@ setup(
|
||||||
'requests',
|
'requests',
|
||||||
'whoosh',
|
'whoosh',
|
||||||
'XStatic-Select2',
|
'XStatic-Select2',
|
||||||
|
'python-dateutil',
|
||||||
],
|
],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
cmdclass={
|
cmdclass={
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
# welco - multichannel request processing
|
||||||
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import django_webtest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app(request):
|
||||||
|
wtm = django_webtest.WebTestMixin()
|
||||||
|
wtm._patch_settings()
|
||||||
|
request.addfinalizer(wtm._unpatch_settings)
|
||||||
|
return django_webtest.DjangoTestApp()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user(db):
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
user = User.objects.create(username='toto')
|
||||||
|
user.set_password('toto')
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mail_group(db, settings, user):
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
# add mail group to default user
|
||||||
|
group = Group.objects.create(name='mail')
|
||||||
|
user.groups.add(group)
|
||||||
|
|
||||||
|
# define authorization of mail group on mail channel
|
||||||
|
channel_roles = getattr(settings, 'CHANNEL_ROLES', {})
|
||||||
|
mail_roles = channel_roles.setdefault('mail', [])
|
||||||
|
mail_roles.append('mail')
|
||||||
|
return group
|
|
@ -0,0 +1,249 @@
|
||||||
|
# welco - multichannel request processing
|
||||||
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from httmock import urlmatch, HTTMock
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMock(object):
|
||||||
|
def __init__(self, netloc):
|
||||||
|
self.netloc = netloc
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.requests = []
|
||||||
|
self.responses = []
|
||||||
|
|
||||||
|
def next_response(self):
|
||||||
|
response, self.responses = self.responses[0], self.responses[1:]
|
||||||
|
return response
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ctx_manager(self):
|
||||||
|
'''Create an HTTMock context manager for all endpoints of a mocked Maarch instance'''
|
||||||
|
endpoints = []
|
||||||
|
for attribute, value in self.__class__.__dict__.items():
|
||||||
|
if hasattr(value, 'path'):
|
||||||
|
value = getattr(self, attribute)
|
||||||
|
match_decorator = urlmatch(netloc=self.netloc, path=value.path)
|
||||||
|
print value, self.netloc, '^/rest' + value.path
|
||||||
|
endpoints.append(match_decorator(value))
|
||||||
|
return HTTMock(*endpoints)
|
||||||
|
|
||||||
|
|
||||||
|
class MaarchMock(BaseMock):
|
||||||
|
def list_endpoint(self, url, request):
|
||||||
|
self.requests.append(('list_endpoint', url, request, json.loads(request.body)))
|
||||||
|
return {
|
||||||
|
'content': json.dumps(self.next_response()),
|
||||||
|
'headers': {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
list_endpoint.path = '^/rest/res/list$'
|
||||||
|
|
||||||
|
def update_external_infos(self, url, request):
|
||||||
|
self.requests.append(('update_external_infos', url, request, json.loads(request.body)))
|
||||||
|
return json.dumps({})
|
||||||
|
update_external_infos.path = '^/rest/res/externalInfos$'
|
||||||
|
|
||||||
|
def update_status(self, url, request):
|
||||||
|
self.requests.append(('update_status', url, request, json.loads(request.body)))
|
||||||
|
return {
|
||||||
|
'content': json.dumps(self.next_response()),
|
||||||
|
'headers': {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
update_status.path = '^/rest/res/resource/status$'
|
||||||
|
|
||||||
|
def post_courrier(self, url, request):
|
||||||
|
self.requests.append(('post_courrier', url, request, json.loads(request.body)))
|
||||||
|
post_courrier.path = '^/rest/res$'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def maarch(settings, mail_group):
|
||||||
|
# configure maarch server
|
||||||
|
settings.MAARCH_FEED = {
|
||||||
|
'ENABLE': True,
|
||||||
|
'URL': 'http://maarch.example.net/',
|
||||||
|
'USERNAME': 'admin',
|
||||||
|
'PASSWORD': 'admin',
|
||||||
|
}
|
||||||
|
return MaarchMock('maarch.example.net')
|
||||||
|
|
||||||
|
|
||||||
|
class WcsMock(BaseMock):
|
||||||
|
def api_formdefs(self, url, request):
|
||||||
|
return json.dumps({
|
||||||
|
'data': [{
|
||||||
|
'slug': 'slug1',
|
||||||
|
'title': 'title1',
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
api_formdefs.path = '^/api/formdefs/$'
|
||||||
|
|
||||||
|
def json(self, url, request):
|
||||||
|
return json.dumps({
|
||||||
|
'data': [{
|
||||||
|
'slug': 'slug1',
|
||||||
|
'title': 'title1',
|
||||||
|
'category': 'category1',
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
json.path = '^/json$'
|
||||||
|
|
||||||
|
def api_formdefs_slug1_schema(self, url, request):
|
||||||
|
return json.dumps({
|
||||||
|
})
|
||||||
|
api_formdefs_slug1_schema.path = '^/api/formdefs/slug-1/schema$'
|
||||||
|
|
||||||
|
def api_formdefs_slug1_submit(self, url, request):
|
||||||
|
return json.dumps({
|
||||||
|
'err': 0,
|
||||||
|
'data': {
|
||||||
|
'id': 1,
|
||||||
|
'backoffice_url': 'http://wcs.example.net/slug-1/1',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
api_formdefs_slug1_submit.path = '^/api/formdefs/slug-1/submit$'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def wcs(settings):
|
||||||
|
settings.KNOWN_SERVICES = {
|
||||||
|
'wcs': {
|
||||||
|
'demarches': {
|
||||||
|
'url': 'http://wcs.example.net/',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return WcsMock('wcs.example.net')
|
||||||
|
|
||||||
|
|
||||||
|
def test_utils(maarch):
|
||||||
|
from welco.sources.mail.utils import get_maarch
|
||||||
|
|
||||||
|
welco_maarch_obj = get_maarch()
|
||||||
|
assert welco_maarch_obj.url == 'http://maarch.example.net/'
|
||||||
|
assert welco_maarch_obj.username == 'admin'
|
||||||
|
assert welco_maarch_obj.password == 'admin'
|
||||||
|
assert welco_maarch_obj.grc_status == 'GRC'
|
||||||
|
assert welco_maarch_obj.grc_received_status == 'GRC_TRT'
|
||||||
|
assert welco_maarch_obj.grc_send_status == 'GRCSENT'
|
||||||
|
assert welco_maarch_obj.grc_refused_status == 'GRCREFUSED'
|
||||||
|
|
||||||
|
|
||||||
|
PDF_MOCK = '%PDF-1.4 ...'
|
||||||
|
|
||||||
|
|
||||||
|
def test_feed(app, maarch, wcs, user):
|
||||||
|
import base64
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from welco.sources.mail.models import Mail
|
||||||
|
|
||||||
|
app.set_user(user.username)
|
||||||
|
response = app.get('/').follow()
|
||||||
|
# no mail
|
||||||
|
assert len(response.pyquery('li[data-external-id]')) == 0
|
||||||
|
|
||||||
|
# feed mails from maarch
|
||||||
|
with maarch.ctx_manager:
|
||||||
|
# list request
|
||||||
|
maarch.responses.append({
|
||||||
|
'resources': [
|
||||||
|
{
|
||||||
|
'res_id': 1,
|
||||||
|
'fileBase64Content': base64.b64encode(PDF_MOCK),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
# update status request
|
||||||
|
maarch.responses.append({})
|
||||||
|
# last list request
|
||||||
|
maarch.responses.append({'resources': []})
|
||||||
|
call_command('feed_mail_maarch')
|
||||||
|
assert len(maarch.requests) == 3
|
||||||
|
assert maarch.requests[0][3] == {
|
||||||
|
'select': '*',
|
||||||
|
'clause': "status='GRC'",
|
||||||
|
'withFile': True,
|
||||||
|
'orderBy': ['res_id'],
|
||||||
|
'limit': 10,
|
||||||
|
}
|
||||||
|
assert maarch.requests[1][3] == {
|
||||||
|
'resId': [1],
|
||||||
|
'status': 'GRC_TRT',
|
||||||
|
}
|
||||||
|
assert maarch.requests[2][3] == {
|
||||||
|
'select': '*',
|
||||||
|
'clause': "status='GRC'",
|
||||||
|
'withFile': True,
|
||||||
|
'orderBy': ['res_id'],
|
||||||
|
'limit': 10,
|
||||||
|
}
|
||||||
|
response = app.get('/').follow()
|
||||||
|
|
||||||
|
# new mail is visible
|
||||||
|
assert len(response.pyquery('li[data-external-id]')) == 1
|
||||||
|
assert len(response.pyquery('li[data-external-id=maarch-1]')) == 1
|
||||||
|
|
||||||
|
# start qualification
|
||||||
|
maarch.clear()
|
||||||
|
pk = Mail.objects.get().pk
|
||||||
|
with wcs.ctx_manager, maarch.ctx_manager:
|
||||||
|
source_type = str(ContentType.objects.get_for_model(Mail).pk),
|
||||||
|
source_pk = str(pk)
|
||||||
|
|
||||||
|
response = app.get('/ajax/qualification', params={
|
||||||
|
'source_type': source_type,
|
||||||
|
'source_pk': source_pk,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert len(response.pyquery('a[data-association-pk]')) == 0
|
||||||
|
response = app.post('/ajax/qualification', params={
|
||||||
|
'source_type': source_type,
|
||||||
|
'source_pk': str(pk),
|
||||||
|
'formdef_reference': 'demarches:slug-1',
|
||||||
|
})
|
||||||
|
|
||||||
|
# verify qualification was done
|
||||||
|
assert len(response.pyquery('a[data-association-pk]')) == 1
|
||||||
|
association_pk = response.pyquery('a[data-association-pk]')[0].attrib['data-association-pk']
|
||||||
|
|
||||||
|
response = app.post('/ajax/create-formdata/%s' % association_pk)
|
||||||
|
assert len(maarch.requests) == 1
|
||||||
|
assert maarch.requests[0][3] == {
|
||||||
|
'status': 'GRCSENT',
|
||||||
|
'externalInfos': [
|
||||||
|
{
|
||||||
|
'external_id': 1,
|
||||||
|
'external_link': 'http://wcs.example.net/slug-1/1',
|
||||||
|
'res_id': 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_is_noop():
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
call_command('feed_mail_maarch')
|
|
@ -1,5 +1,5 @@
|
||||||
# welco - multichannel request processing
|
# welco - multichannel request processing
|
||||||
# Copyright (C) 2015 Entr'ouvert
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
# under the terms of the GNU Affero General Public License as published
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -28,16 +28,6 @@ from welco.sources.phone import models
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def user():
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
|
||||||
user = User.objects.create(username='toto')
|
|
||||||
user.set_password('toto')
|
|
||||||
user.save()
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def test_call_start_stop(client):
|
def test_call_start_stop(client):
|
||||||
assert models.PhoneCall.objects.count() == 0
|
assert models.PhoneCall.objects.count() == 0
|
||||||
payload = {
|
payload = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# welco - multichannel request processing
|
# welco - multichannel request processing
|
||||||
# Copyright (C) 2015 Entr'ouvert
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
# under the terms of the GNU Affero General Public License as published
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import django.apps
|
import django.apps
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(django.apps.AppConfig):
|
class AppConfig(django.apps.AppConfig):
|
||||||
name = 'welco.sources.mail'
|
name = 'welco.sources.mail'
|
||||||
|
|
||||||
|
@ -23,4 +24,29 @@ class AppConfig(django.apps.AppConfig):
|
||||||
from . import urls
|
from . import urls
|
||||||
return urls.urlpatterns
|
return urls.urlpatterns
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from welco.qualif.models import Association
|
||||||
|
from django.db.models import signals
|
||||||
|
|
||||||
|
signals.post_save.connect(self.association_post_save,
|
||||||
|
sender=Association)
|
||||||
|
|
||||||
|
def association_post_save(self, sender, instance, **kwargs):
|
||||||
|
from .utils import get_maarch
|
||||||
|
|
||||||
|
if not instance.formdata_id:
|
||||||
|
return
|
||||||
|
source = instance.source
|
||||||
|
if not hasattr(source, 'external_id'):
|
||||||
|
return
|
||||||
|
external_id = source.external_id
|
||||||
|
if not external_id.startswith('maarch-'):
|
||||||
|
return
|
||||||
|
maarch_pk = int(external_id.split('-', 1)[-1])
|
||||||
|
maarch = get_maarch()
|
||||||
|
maarch.set_grc_sent_status(
|
||||||
|
mail_pk=maarch_pk,
|
||||||
|
formdata_id=instance.formdata_id,
|
||||||
|
formdata_url_backoffice=instance.formdata_url_backoffice)
|
||||||
|
|
||||||
default_app_config = 'welco.sources.mail.AppConfig'
|
default_app_config = 'welco.sources.mail.AppConfig'
|
||||||
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
# welco - multichannel request processing
|
||||||
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import urlparse
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from dateutil.parser import parse as parse_datetime
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from requests.packages.urllib3.util.retry import Retry
|
||||||
|
|
||||||
|
|
||||||
|
class MaarchError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MaarchCourrier(object):
|
||||||
|
url = None
|
||||||
|
username = None
|
||||||
|
password = None
|
||||||
|
default_limit = 100
|
||||||
|
max_retries = 3
|
||||||
|
|
||||||
|
def __init__(self, url, username, password):
|
||||||
|
self.url = url
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<MaarchCourrier url:%s>' % self.url
|
||||||
|
|
||||||
|
class Courrier(object):
|
||||||
|
content = None
|
||||||
|
format = None
|
||||||
|
status = None
|
||||||
|
|
||||||
|
def __init__(self, maarch_courrier, **kwargs):
|
||||||
|
self.maarch_courrier = maarch_courrier
|
||||||
|
self.pk = kwargs.pop('res_id', None)
|
||||||
|
# decode file content
|
||||||
|
if 'fileBase64Content' in kwargs:
|
||||||
|
kwargs['content'] = base64.b64decode(kwargs.pop('fileBase64Content'))
|
||||||
|
# decode date fields
|
||||||
|
for key in kwargs:
|
||||||
|
if key.endswith('_date') and kwargs[key]:
|
||||||
|
kwargs[key] = parse_datetime(kwargs[key])
|
||||||
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
descriptions = []
|
||||||
|
for key in ['pk', 'status']:
|
||||||
|
if getattr(self, key, None):
|
||||||
|
descriptions.append('%s:%s' % (key, getattr(self, key)))
|
||||||
|
return '<Courrier %s>' % ' '.join(descriptions)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new_with_file(cls, maarch_courrier, content, format, status, **kwargs):
|
||||||
|
if hasattr(content, 'read'):
|
||||||
|
content = content.read()
|
||||||
|
else:
|
||||||
|
content = content
|
||||||
|
return cls(maarch_courrier, content=content, format=format, status=status, **kwargs)
|
||||||
|
|
||||||
|
def post_serialize(self):
|
||||||
|
payload = {}
|
||||||
|
assert self.content
|
||||||
|
assert self.status
|
||||||
|
payload['encodedFile'] = base64.b64encode(self.content)
|
||||||
|
payload['collId'] = 'letterbox_coll'
|
||||||
|
payload['table'] = 'res_letterbox'
|
||||||
|
payload['fileFormat'] = self.format
|
||||||
|
payload['data'] = d = []
|
||||||
|
excluded_keys = ['content', 'format', 'status', 'maarch_courrier', 'pk']
|
||||||
|
data = {key: self.__dict__[key] for key in self.__dict__ if key not in excluded_keys}
|
||||||
|
if data:
|
||||||
|
for key, value in data.iteritems():
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
d.append({'column': key, 'value': value, 'type': 'string'})
|
||||||
|
elif isinstance(value, int):
|
||||||
|
d.append({'column': key, 'value': str(value), 'type': 'int'})
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
payload['status'] = self.status
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def get_serialize(self):
|
||||||
|
d = {'res_id': self.pk}
|
||||||
|
for key in self.__dict__:
|
||||||
|
if key in ['pk', 'maarch_courrier']:
|
||||||
|
continue
|
||||||
|
value = getattr(self, key)
|
||||||
|
if key == 'content':
|
||||||
|
value = base64.b64encode(value)
|
||||||
|
key = 'fileBase64Content'
|
||||||
|
if key.endswith('_date'):
|
||||||
|
value = value.isoformat()
|
||||||
|
d[key] = value
|
||||||
|
return d
|
||||||
|
|
||||||
|
def new_courrier_with_file(self, content, format, status, **kwargs):
|
||||||
|
return self.Courrier.new_with_file(self, content, format, status, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self):
|
||||||
|
s = requests.Session()
|
||||||
|
if self.username and self.password:
|
||||||
|
s.auth = (self.username, self.password)
|
||||||
|
retry = Retry(
|
||||||
|
total=self.max_retries,
|
||||||
|
read=self.max_retries,
|
||||||
|
connect=self.max_retries,
|
||||||
|
backoff_factor=0.5,
|
||||||
|
status_forcelist=(500, 502, 504)
|
||||||
|
)
|
||||||
|
adapter = HTTPAdapter(max_retries=retry)
|
||||||
|
s.mount('http://', adapter)
|
||||||
|
s.mount('https://', adapter)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def post_json(self, url, payload, verb='post'):
|
||||||
|
try:
|
||||||
|
method = getattr(self.session, verb)
|
||||||
|
response = method(url, json=payload)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise MaarchError('HTTP request to maarch failed', e, payload)
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise MaarchError('HTTP request to maarch failed', e, payload, repr(response.content[:1000]))
|
||||||
|
try:
|
||||||
|
response_payload = response.json()
|
||||||
|
except ValueError:
|
||||||
|
raise MaarchError('maarch returned non-JSON data', repr(response.content[:1000]), payload)
|
||||||
|
return response_payload
|
||||||
|
|
||||||
|
def put_json(self, url, payload):
|
||||||
|
return self.post_json(url, payload, verb='put')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_url(self):
|
||||||
|
return urlparse.urljoin(self.url, 'rest/res/list')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_external_infos_url(self):
|
||||||
|
return urlparse.urljoin(self.url, 'rest/res/externalInfos')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_status_url(self):
|
||||||
|
return urlparse.urljoin(self.url, 'rest/res/resource/status')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def post_courrier_url(self):
|
||||||
|
return urlparse.urljoin(self.url, 'rest/res')
|
||||||
|
|
||||||
|
def get_courriers(self, clause, fields=None, limit=None, include_file=False, order_by=None):
|
||||||
|
if fields:
|
||||||
|
# res_id is mandatory
|
||||||
|
fields = set(fields)
|
||||||
|
fields.add('res_id')
|
||||||
|
fields = ','.join(fields) if fields else '*'
|
||||||
|
limit = limit or self.default_limit
|
||||||
|
order_by = order_by or []
|
||||||
|
response = self.post_json(self.list_url, {
|
||||||
|
'select': fields,
|
||||||
|
'clause': clause,
|
||||||
|
'limit': limit,
|
||||||
|
'withFile': include_file,
|
||||||
|
'orderBy': order_by,
|
||||||
|
})
|
||||||
|
if not hasattr(response.get('resources'), 'append'):
|
||||||
|
raise MaarchError('missing resources field or bad type', response)
|
||||||
|
return [self.Courrier(self, **resource) for resource in response['resources']]
|
||||||
|
|
||||||
|
def update_external_infos(self, courriers, status):
|
||||||
|
if not courriers:
|
||||||
|
return
|
||||||
|
external_infos = []
|
||||||
|
payload = {
|
||||||
|
'externalInfos': external_infos,
|
||||||
|
'status': status,
|
||||||
|
}
|
||||||
|
for courrier in courriers:
|
||||||
|
assert courrier.pk, 'courrier must already exist in Maarch and have a pk'
|
||||||
|
external_info = {'res_id': courrier.pk}
|
||||||
|
if getattr(courrier, 'external_id', None):
|
||||||
|
external_info['external_id'] = courrier.external_id
|
||||||
|
if getattr(courrier, 'external_link', None):
|
||||||
|
external_info['external_link'] = courrier.external_link
|
||||||
|
external_infos.append(external_info)
|
||||||
|
response = self.put_json(self.update_external_infos_url, payload)
|
||||||
|
if 'errors' in response:
|
||||||
|
raise MaarchError('update_external_infos failed with errors', response['errors'], response)
|
||||||
|
|
||||||
|
def update_status(self, courriers, status, history_message=None):
|
||||||
|
if not courriers:
|
||||||
|
return
|
||||||
|
res_ids = []
|
||||||
|
for courrier in courriers:
|
||||||
|
assert courrier.pk
|
||||||
|
res_ids.append(courrier.pk)
|
||||||
|
payload = {
|
||||||
|
'status': status,
|
||||||
|
'resId': res_ids,
|
||||||
|
}
|
||||||
|
if history_message:
|
||||||
|
payload['historyMessage'] = history_message
|
||||||
|
response = self.put_json(self.update_status_url, payload)
|
||||||
|
|
||||||
|
if 'errors' in response:
|
||||||
|
raise MaarchError('update_status failed with errors', response['errors'], response)
|
||||||
|
|
||||||
|
def post_courrier(self, courrier):
|
||||||
|
response = self.post_json(self.post_courrier_url, courrier.post_serialize())
|
||||||
|
if 'errors' in response:
|
||||||
|
raise MaarchError('update_external_infos failed with errors', response['errors'], response)
|
||||||
|
if 'resId' not in response:
|
||||||
|
raise MaarchError('update_external_infos failed with errors, missing resId', response)
|
||||||
|
courrier.pk = response['resId']
|
||||||
|
return courrier
|
|
@ -0,0 +1,61 @@
|
||||||
|
# welco - multichannel request processing
|
||||||
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from optparse import make_option
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from ...models import Mail
|
||||||
|
from ...utils import get_maarch
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Inject mail coming from Maarch into welco.
|
||||||
|
|
||||||
|
Only mail with a status "GRC" are injected,
|
||||||
|
After injection, their status is immediately changed to "GRC_TRT".
|
||||||
|
After injection in w.c.s., their status is changed to "GRCSENT" and an
|
||||||
|
id and an URL of the request in w.c.s. is attached to the mail in
|
||||||
|
Maarch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
verbosity = kwargs['verbosity']
|
||||||
|
maarch = get_maarch()
|
||||||
|
if not maarch:
|
||||||
|
if verbosity > 1:
|
||||||
|
self.stdout.write('Maarch is not configured.')
|
||||||
|
return
|
||||||
|
|
||||||
|
maarch_mails = maarch.get_mails()
|
||||||
|
count = 0
|
||||||
|
while maarch_mails:
|
||||||
|
with transaction.atomic():
|
||||||
|
for maarch_mail in maarch_mails:
|
||||||
|
Mail.objects.create(
|
||||||
|
content=ContentFile(maarch_mail.content, name='maarch-%s' % maarch_mail.pk),
|
||||||
|
external_id='maarch-%s' % str(maarch_mail.pk), # res_id
|
||||||
|
)
|
||||||
|
# update maarch inside transaction, if it fails all imports will be
|
||||||
|
# rollbacked
|
||||||
|
maarch.set_grc_received_status(maarch_mails)
|
||||||
|
count += len(maarch_mails)
|
||||||
|
maarch_mails = maarch.get_mails()
|
||||||
|
if verbosity > 1:
|
||||||
|
self.stdout.write('Injected %d mails from %s.' % (count, maarch.url))
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mail', '0011_mail_reference'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mail',
|
||||||
|
name='external_id',
|
||||||
|
field=models.CharField(max_length=32, null=True, verbose_name='External Id'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,5 +1,5 @@
|
||||||
# welco - multichannel request processing
|
# welco - multichannel request processing
|
||||||
# Copyright (C) 2015 Entr'ouvert
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
# under the terms of the GNU Affero General Public License as published
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -42,6 +42,7 @@ class Mail(models.Model):
|
||||||
registered_mail_number = models.CharField(_('Registered Mail Number'),
|
registered_mail_number = models.CharField(_('Registered Mail Number'),
|
||||||
null=True, max_length=50)
|
null=True, max_length=50)
|
||||||
note = models.TextField(_('Note'), null=True)
|
note = models.TextField(_('Note'), null=True)
|
||||||
|
external_id = models.CharField(_('External Id'), null=True, max_length=32)
|
||||||
|
|
||||||
# used only if settings.FLAVOURS contains 'alfortville'
|
# used only if settings.FLAVOURS contains 'alfortville'
|
||||||
reference = models.CharField(_('Reference'), null=True, max_length=30)
|
reference = models.CharField(_('Reference'), null=True, max_length=30)
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
data-registered-mail-number="{{ mail.registered_mail_number|default:"" }}"
|
data-registered-mail-number="{{ mail.registered_mail_number|default:"" }}"
|
||||||
data-reference="{{ mail.reference|default:"" }}"
|
data-reference="{{ mail.reference|default:"" }}"
|
||||||
data-subject="{{ mail.subject|default:"" }}"
|
data-subject="{{ mail.subject|default:"" }}"
|
||||||
|
data-external-id="{{ mail.external_id|default:"" }}"
|
||||||
>{{ mail.creation_timestamp|date:"d/m/Y" }}
|
>{{ mail.creation_timestamp|date:"d/m/Y" }}
|
||||||
{{mail.contact_name}}
|
{{mail.contact_name}}
|
||||||
{% for association in mail.associations.all %}
|
{% for association in mail.associations.all %}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
# welco - multichannel request processing
|
||||||
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
# by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .maarch import MaarchCourrier
|
||||||
|
|
||||||
|
|
||||||
|
class WelcoMaarchCourrier(MaarchCourrier):
|
||||||
|
def __init__(self, url, username, password, grc_status,
|
||||||
|
grc_received_status, grc_send_status, grc_refused_status,
|
||||||
|
batch_size=10):
|
||||||
|
super(WelcoMaarchCourrier, self).__init__(url, username, password)
|
||||||
|
self.grc_status = grc_status
|
||||||
|
self.grc_received_status = grc_received_status
|
||||||
|
self.grc_send_status = grc_send_status
|
||||||
|
self.grc_refused_status = grc_refused_status
|
||||||
|
self.batch_size = batch_size
|
||||||
|
|
||||||
|
def get_mails(self):
|
||||||
|
return self.get_courriers(
|
||||||
|
clause="status='%s'" % self.grc_status,
|
||||||
|
include_file=True,
|
||||||
|
order_by=['res_id'],
|
||||||
|
limit=self.batch_size)
|
||||||
|
|
||||||
|
def get_mail(self, mail_id):
|
||||||
|
return self.get_courriers(clause="res_id=%s" % mail_id)[0]
|
||||||
|
|
||||||
|
def set_grc_received_status(self, mails):
|
||||||
|
self.update_status(mails, self.grc_received_status)
|
||||||
|
|
||||||
|
def set_grc_sent_status(self, mail_pk, formdata_id, formdata_url_backoffice):
|
||||||
|
mail = self.Courrier(self, pk=mail_pk)
|
||||||
|
mail.external_id = formdata_id
|
||||||
|
mail.external_link = formdata_url_backoffice
|
||||||
|
self.update_external_infos([mail], self.grc_send_status)
|
||||||
|
|
||||||
|
def set_grc_refused_status(self, mail_pk):
|
||||||
|
mail = self.Courrier(self, pk=mail_pk)
|
||||||
|
self.update_status([mail], self.grc_refused_status)
|
||||||
|
|
||||||
|
|
||||||
|
def get_maarch():
|
||||||
|
config = getattr(settings, 'MAARCH_FEED', {})
|
||||||
|
if not config.get('ENABLE'):
|
||||||
|
return
|
||||||
|
url = config['URL']
|
||||||
|
username = config['USERNAME']
|
||||||
|
password = config['PASSWORD']
|
||||||
|
return WelcoMaarchCourrier(
|
||||||
|
url=url,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
grc_status=config.get('STATUS_GRC', 'GRC'),
|
||||||
|
grc_received_status=config.get('STATUS_RECEIVED', 'GRC_TRT'),
|
||||||
|
grc_send_status=config.get('STATUS_SEND', 'GRCSENT'),
|
||||||
|
grc_refused_status=config.get('STATUS_REFUSED', 'GRCREFUSED'))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# welco - multichannel request processing
|
# welco - multichannel request processing
|
||||||
# Copyright (C) 2015 Entr'ouvert
|
# Copyright (C) 2018 Entr'ouvert
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it
|
# This program is free software: you can redistribute it and/or modify it
|
||||||
# under the terms of the GNU Affero General Public License as published
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
@ -26,11 +27,16 @@ from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
from django.db.transaction import atomic
|
||||||
|
|
||||||
from welco.utils import response_for_json
|
from welco.utils import response_for_json
|
||||||
|
|
||||||
from .models import Mail
|
from .models import Mail
|
||||||
from .forms import MailQualificationForm
|
from .forms import MailQualificationForm
|
||||||
|
from .utils import get_maarch
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def viewer(request, *args, **kwargs):
|
def viewer(request, *args, **kwargs):
|
||||||
if not 'file' in request.GET:
|
if not 'file' in request.GET:
|
||||||
|
@ -124,7 +130,18 @@ def note(request, *args, **kwargs):
|
||||||
@login_required
|
@login_required
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def reject(request, *args, **kwargs):
|
def reject(request, *args, **kwargs):
|
||||||
Mail.objects.filter(id=request.POST['source_pk']).delete()
|
maarch = get_maarch()
|
||||||
|
mail = Mail.objects.filter(id=request.POST['source_pk']).first()
|
||||||
|
if mail:
|
||||||
|
try:
|
||||||
|
with atomic():
|
||||||
|
if maarch and mail.external_id and mail.external_id.startswith('maarch-'):
|
||||||
|
mail_pk = mail.external_id.split('-', 1)[1]
|
||||||
|
maarch.set_grc_refused_status(mail_pk)
|
||||||
|
mail.delete()
|
||||||
|
except Exception:
|
||||||
|
logger.exception('rejection request to maarch failed')
|
||||||
|
messages.error(request, _('Rejection request to Maarch failed'))
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue