diff --git a/debian/control b/debian/control
index 532652b..174a067 100644
--- a/debian/control
+++ b/debian/control
@@ -11,7 +11,7 @@ Architecture: all
Depends: ${misc:Depends}, ${python:Depends},
python-django (>= 1.7),
python-gadjo,
- python-requests,
+ python-requests (>= 2.11),
python-django-haystack (>= 2.4.0),
python-django-reversion (>= 2.0.12),
python-django-taggit (>= 0.17.4),
diff --git a/setup.py b/setup.py
index c9fcd70..9938f0d 100644
--- a/setup.py
+++ b/setup.py
@@ -106,6 +106,7 @@ setup(
'requests',
'whoosh',
'XStatic-Select2',
+ 'python-dateutil',
],
zip_safe=False,
cmdclass={
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..d6b91e7
--- /dev/null
+++ b/tests/conftest.py
@@ -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 .
+
+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
diff --git a/tests/test_source_maarch.py b/tests/test_source_maarch.py
new file mode 100644
index 0000000..a41405c
--- /dev/null
+++ b/tests/test_source_maarch.py
@@ -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 .
+
+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')
diff --git a/tests/test_source_phone.py b/tests/test_source_phone.py
index d344a44..80cf049 100644
--- a/tests/test_source_phone.py
+++ b/tests/test_source_phone.py
@@ -1,5 +1,5 @@
# 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
# 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
-@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):
assert models.PhoneCall.objects.count() == 0
payload = {
diff --git a/welco/sources/mail/__init__.py b/welco/sources/mail/__init__.py
index aec1225..60d86e0 100644
--- a/welco/sources/mail/__init__.py
+++ b/welco/sources/mail/__init__.py
@@ -1,5 +1,5 @@
# 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
# under the terms of the GNU Affero General Public License as published
@@ -16,6 +16,7 @@
import django.apps
+
class AppConfig(django.apps.AppConfig):
name = 'welco.sources.mail'
@@ -23,4 +24,29 @@ class AppConfig(django.apps.AppConfig):
from . import urls
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'
diff --git a/welco/sources/mail/maarch.py b/welco/sources/mail/maarch.py
new file mode 100644
index 0000000..8021a88
--- /dev/null
+++ b/welco/sources/mail/maarch.py
@@ -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 .
+
+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 '' % 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 '' % ' '.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
diff --git a/welco/sources/mail/management/commands/feed_mail_maarch.py b/welco/sources/mail/management/commands/feed_mail_maarch.py
new file mode 100644
index 0000000..6166600
--- /dev/null
+++ b/welco/sources/mail/management/commands/feed_mail_maarch.py
@@ -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 .
+
+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))
diff --git a/welco/sources/mail/migrations/0012_mail_external_id.py b/welco/sources/mail/migrations/0012_mail_external_id.py
new file mode 100644
index 0000000..430f910
--- /dev/null
+++ b/welco/sources/mail/migrations/0012_mail_external_id.py
@@ -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'),
+ ),
+ ]
diff --git a/welco/sources/mail/models.py b/welco/sources/mail/models.py
index ec985e9..494b16e 100644
--- a/welco/sources/mail/models.py
+++ b/welco/sources/mail/models.py
@@ -1,5 +1,5 @@
# 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
# 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'),
null=True, max_length=50)
note = models.TextField(_('Note'), null=True)
+ external_id = models.CharField(_('External Id'), null=True, max_length=32)
# used only if settings.FLAVOURS contains 'alfortville'
reference = models.CharField(_('Reference'), null=True, max_length=30)
diff --git a/welco/sources/mail/templates/welco/mail_home.html b/welco/sources/mail/templates/welco/mail_home.html
index b8571cb..12d9c9d 100644
--- a/welco/sources/mail/templates/welco/mail_home.html
+++ b/welco/sources/mail/templates/welco/mail_home.html
@@ -13,6 +13,7 @@
data-registered-mail-number="{{ mail.registered_mail_number|default:"" }}"
data-reference="{{ mail.reference|default:"" }}"
data-subject="{{ mail.subject|default:"" }}"
+ data-external-id="{{ mail.external_id|default:"" }}"
>{{ mail.creation_timestamp|date:"d/m/Y" }}
{{mail.contact_name}}
{% for association in mail.associations.all %}
diff --git a/welco/sources/mail/utils.py b/welco/sources/mail/utils.py
new file mode 100644
index 0000000..6c59add
--- /dev/null
+++ b/welco/sources/mail/utils.py
@@ -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 .
+
+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'))
+
diff --git a/welco/sources/mail/views.py b/welco/sources/mail/views.py
index 1312781..d162019 100644
--- a/welco/sources/mail/views.py
+++ b/welco/sources/mail/views.py
@@ -1,5 +1,5 @@
# 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
# under the terms of the GNU Affero General Public License as published
@@ -15,6 +15,7 @@
# along with this program. If not, see .
import json
+import logging
from django import template
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.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
+from django.db.transaction import atomic
from welco.utils import response_for_json
from .models import Mail
from .forms import MailQualificationForm
+from .utils import get_maarch
+
+
+logger = logging.getLogger(__name__)
def viewer(request, *args, **kwargs):
if not 'file' in request.GET:
@@ -124,7 +130,18 @@ def note(request, *args, **kwargs):
@login_required
@csrf_exempt
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()