From a4ae8845e9c92fccc05a5ad80873f80f211c528f Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Sat, 19 May 2018 16:38:57 +0200 Subject: [PATCH] mail: feed from MaarchCourrier (#22550) --- debian/control | 2 +- setup.py | 1 + tests/conftest.py | 51 ++++ tests/test_source_maarch.py | 249 ++++++++++++++++++ tests/test_source_phone.py | 12 +- welco/sources/mail/__init__.py | 28 +- welco/sources/mail/maarch.py | 233 ++++++++++++++++ .../management/commands/feed_mail_maarch.py | 61 +++++ .../mail/migrations/0012_mail_external_id.py | 19 ++ welco/sources/mail/models.py | 3 +- .../mail/templates/welco/mail_home.html | 1 + welco/sources/mail/utils.py | 72 +++++ welco/sources/mail/views.py | 21 +- 13 files changed, 737 insertions(+), 16 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_source_maarch.py create mode 100644 welco/sources/mail/maarch.py create mode 100644 welco/sources/mail/management/commands/feed_mail_maarch.py create mode 100644 welco/sources/mail/migrations/0012_mail_external_id.py create mode 100644 welco/sources/mail/utils.py 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()