From 347570812498e4eb279862305202a4c1bbe1adf5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 29 Apr 2021 14:48:45 +0200
Subject: [PATCH] misc: remove newsletter app (#53541)

---
 combo/apps/newsletters/README                 |  75 -----
 combo/apps/newsletters/__init__.py            |  31 --
 combo/apps/newsletters/forms.py               |  83 ------
 .../newsletters/migrations/0001_initial.py    |  58 ----
 .../0002_newsletterscell_extra_css_class.py   |  19 --
 ...3_newsletterscell_last_update_timestamp.py |  23 --
 combo/apps/newsletters/migrations/__init__.py |   0
 combo/apps/newsletters/models.py              | 165 -----------
 .../templates/newsletters/newsletters.html    |  43 ---
 combo/apps/newsletters/urls.py                |  12 -
 combo/apps/newsletters/views.py               |  51 ----
 combo/settings.py                             |   2 -
 tests/settings.py                             |   1 -
 tests/test_manager.py                         |   8 +-
 tests/test_newsletters_cell.py                | 275 ------------------
 tests/test_search.py                          |   2 +-
 16 files changed, 5 insertions(+), 843 deletions(-)
 delete mode 100644 combo/apps/newsletters/README
 delete mode 100644 combo/apps/newsletters/__init__.py
 delete mode 100644 combo/apps/newsletters/forms.py
 delete mode 100644 combo/apps/newsletters/migrations/0001_initial.py
 delete mode 100644 combo/apps/newsletters/migrations/0002_newsletterscell_extra_css_class.py
 delete mode 100644 combo/apps/newsletters/migrations/0003_newsletterscell_last_update_timestamp.py
 delete mode 100644 combo/apps/newsletters/migrations/__init__.py
 delete mode 100644 combo/apps/newsletters/models.py
 delete mode 100644 combo/apps/newsletters/templates/newsletters/newsletters.html
 delete mode 100644 combo/apps/newsletters/urls.py
 delete mode 100644 combo/apps/newsletters/views.py
 delete mode 100644 tests/test_newsletters_cell.py

diff --git a/combo/apps/newsletters/README b/combo/apps/newsletters/README
deleted file mode 100644
index f6e02cd2..00000000
--- a/combo/apps/newsletters/README
+++ /dev/null
@@ -1,75 +0,0 @@
-Combo newsletters cell
-======================
-
-This cell is enabled by default.
-
-It expects a webservice returning newsletters and user subscriptions in the
-following format:
-
-  [{'id': '1', 'text': 'Democratie locale',
-    'transports': [{'id': 'mail', 'text': 'mail'}]},
-  {'id': '2', 'text': 'Rencontres de quartiers',
-   'transports': [{'id': 'mail', 'text': 'mail'}]},
-  {'id': '3', 'text': 'Environnement',
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'sms', 'text': 'sms'},
-                  {'id': 'rss', 'text': 'rss'}]},
-  {'id': '4', 'text': u'Marchés publics',
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'rss', 'text': 'rss'}]},
-  {'id': '5', 'text': "Offres d'emploi",
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'rss', 'text': 'rss'}]},
-  {'id': '6', 'text': 'Infos créche',
-   'transports': [{'id': 'sms', 'text': 'sms'},
-                  {'id': 'rss', 'text': 'rss'}]},
-  {'id': '7', 'text': 'Familles',
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'sms', 'text': 'sms'}]},
-  {'id': '8', 'text': 'Travaux',
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'sms', 'text': 'sms'},
-                  {'id': 'rss', 'text': 'rss'}]}]
-
-
-The url to the webservice should be provided in the instatiation form. The
-fields **resources_restrictions** and **transports_restrictions** allow to
-filter the newsletters by their name and transport means.
-
-**resources_restrictions** field is a comma separated list of newsletters
-slugs. For example: __rencontres-de-quartiers,infos-creche__.
-
-In this case only the following newsletters will be exposed in the
-subscriptions form:
-  [{'id': '2', 'text': 'Rencontres de quartiers',
-   'transports': [{'id': 'mail', 'text': 'mail'}]},
-   {'id': '6', 'text': 'Infos créche',
-   'transports': [{'id': 'sms', 'text': 'sms'},
-                  {'id': 'rss', 'text': 'rss'}]}]
-
-**transport_restrictions** field is a comma separated list of transport types.
-Example: __sms,rss__
-
-In this case only the newsletters containing one of these transports will be
-shown:
-
-  [{'id': '3', 'text': 'Environnement',
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'sms', 'text': 'sms'},
-                  {'id': 'rss', 'text': 'rss'}]},
-  {'id': '4', 'text': u'Marchés publics',
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'rss', 'text': 'rss'}]},
-  {'id': '5', 'text': "Offres d'emploi",
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'rss', 'text': 'rss'}]},
-  {'id': '6', 'text': 'Infos créche',
-   'transports': [{'id': 'sms', 'text': 'sms'},
-                  {'id': 'rss', 'text': 'rss'}]},
-  {'id': '7', 'text': 'Familles',
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'sms', 'text': 'sms'}]},
-  {'id': '8', 'text': 'Travaux',
-   'transports': [{'id': 'mail', 'text': 'mail'},
-                  {'id': 'sms', 'text': 'sms'},
-                  {'id': 'rss', 'text': 'rss'}]}]
diff --git a/combo/apps/newsletters/__init__.py b/combo/apps/newsletters/__init__.py
deleted file mode 100644
index c6654558..00000000
--- a/combo/apps/newsletters/__init__.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# combo - content management system
-# Copyright (C) 2015  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 django.apps
-from django.utils.translation import ugettext_lazy as _
-
-
-class AppConfig(django.apps.AppConfig):
-    name = 'combo.apps.newsletters'
-    verbose_name = _('Newsletters')
-
-    def get_before_urls(self):
-        from . import urls
-
-        return urls.urlpatterns
-
-
-default_app_config = 'combo.apps.newsletters.AppConfig'
diff --git a/combo/apps/newsletters/forms.py b/combo/apps/newsletters/forms.py
deleted file mode 100644
index e7c14d50..00000000
--- a/combo/apps/newsletters/forms.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# combo - content management system
-# Copyright (C) 2015  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 logging
-
-from django import forms
-from django.utils.translation import ugettext_lazy as _
-
-
-class NewslettersManageForm(forms.Form):
-    def __init__(self, *args, **kwargs):
-        logger = logging.getLogger(__name__)
-        self.request = kwargs.pop('request')
-        self.user = self.request.user
-        self.instance = kwargs.pop('instance')
-        self.themes = set()
-        super(NewslettersManageForm, self).__init__(*args, **kwargs)
-
-        # initialize cleane data in order to be able to add errors
-        self.cleaned_data = {}
-        try:
-            newsletters = self.instance.get_newsletters()
-        except Exception as e:
-            self.add_error(None, _('An error occured while getting newsletters. Please try later.'))
-            logger.error('Error occured while getting newsletters: %r', e)
-            return
-        self.params = {}
-        user_name_id = self.user.get_name_id()
-        if user_name_id:
-            self.params['uuid'] = user_name_id
-
-        # get mobile number from mellon session as it is not user attribute
-        if self.request.session.get('mellon_session'):
-            self.params['mobile'] = self.request.session['mellon_session'].get('mobile', '')
-        try:
-            subscriptions = self.instance.get_subscriptions(self.user, **self.params)
-        except Exception as e:
-            self.add_error(None, _('An error occured while getting subscriptions. Please try later.'))
-            logger.error('Error occured while getting subscriptions: %r', e)
-            return
-
-        for newsletter in newsletters:
-            if not self.instance.check_resource(newsletter['text']):
-                continue
-            choices = []
-            initial = []
-            for transport in newsletter['transports']:
-                if not self.instance.check_transport(transport['id']):
-                    continue
-                self.themes.add((transport['id'], transport['text']))
-                choices.append((transport['id'], ''))
-                if transport in newsletter['transports']:
-                    for subscription in subscriptions:
-                        if subscription['id'] == newsletter['id']:
-                            initial = [t['id'] for t in subscription['transports']]
-                self.fields[newsletter['id']] = forms.MultipleChoiceField(
-                    label=newsletter['text'],
-                    help_text=transport['id'],
-                    choices=choices,
-                    initial=initial,
-                    widget=forms.CheckboxSelectMultiple(),
-                    required=False,
-                )
-
-    def save(self):
-        self.full_clean()
-        subscriptions = []
-        for key, value in self.cleaned_data.items():
-            subscriptions.append({'id': key, 'transports': value})
-        self.instance.set_subscriptions(subscriptions, self.user, **self.params)
diff --git a/combo/apps/newsletters/migrations/0001_initial.py b/combo/apps/newsletters/migrations/0001_initial.py
deleted file mode 100644
index bd8eb6cd..00000000
--- a/combo/apps/newsletters/migrations/0001_initial.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('auth', '0001_initial'),
-        ('data', '0013_parameterscell'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='NewslettersCell',
-            fields=[
-                (
-                    'id',
-                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
-                ),
-                ('placeholder', models.CharField(max_length=20)),
-                ('order', models.PositiveIntegerField()),
-                ('slug', models.SlugField(verbose_name='Slug', blank=True)),
-                ('public', models.BooleanField(default=True, verbose_name='Public')),
-                (
-                    'restricted_to_unlogged',
-                    models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
-                ),
-                ('title', models.CharField(max_length=128, verbose_name='Title')),
-                ('url', models.URLField(max_length=128, verbose_name='Newsletters service url')),
-                (
-                    'resources_restrictions',
-                    models.CharField(
-                        help_text='list of resources(themes) separated by commas',
-                        max_length=1024,
-                        verbose_name='resources restrictions',
-                        blank=True,
-                    ),
-                ),
-                (
-                    'transports_restrictions',
-                    models.CharField(
-                        help_text='list of transports separated by commas',
-                        max_length=1024,
-                        verbose_name='transports restrictions',
-                        blank=True,
-                    ),
-                ),
-                ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
-                ('page', models.ForeignKey(to='data.Page', on_delete=models.CASCADE)),
-            ],
-            options={
-                'verbose_name': 'Newsletters',
-            },
-            bases=(models.Model,),
-        ),
-    ]
diff --git a/combo/apps/newsletters/migrations/0002_newsletterscell_extra_css_class.py b/combo/apps/newsletters/migrations/0002_newsletterscell_extra_css_class.py
deleted file mode 100644
index e9e5de5e..00000000
--- a/combo/apps/newsletters/migrations/0002_newsletterscell_extra_css_class.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('newsletters', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='newsletterscell',
-            name='extra_css_class',
-            field=models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True),
-        ),
-    ]
diff --git a/combo/apps/newsletters/migrations/0003_newsletterscell_last_update_timestamp.py b/combo/apps/newsletters/migrations/0003_newsletterscell_last_update_timestamp.py
deleted file mode 100644
index 521484c8..00000000
--- a/combo/apps/newsletters/migrations/0003_newsletterscell_last_update_timestamp.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-import datetime
-
-from django.db import migrations, models
-from django.utils.timezone import utc
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('newsletters', '0002_newsletterscell_extra_css_class'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='newsletterscell',
-            name='last_update_timestamp',
-            field=models.DateTimeField(default=datetime.datetime.now(utc), auto_now=True),
-            preserve_default=False,
-        ),
-    ]
diff --git a/combo/apps/newsletters/migrations/__init__.py b/combo/apps/newsletters/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/combo/apps/newsletters/models.py b/combo/apps/newsletters/models.py
deleted file mode 100644
index a6d4f580..00000000
--- a/combo/apps/newsletters/models.py
+++ /dev/null
@@ -1,165 +0,0 @@
-# combo - content management system
-# Copyright (C) 2015  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 logging
-
-from django.conf import settings
-from django.db import models
-from django.forms import models as model_forms
-from django.template.defaultfilters import slugify
-from django.utils.http import urlencode
-from django.utils.translation import ugettext_lazy as _
-from requests.exceptions import HTTPError, RequestException
-
-from combo.data.library import register_cell_class
-from combo.data.models import CellBase
-from combo.utils import requests
-
-from .forms import NewslettersManageForm
-
-
-class SubscriptionsSaveError(Exception):
-    pass
-
-
-@register_cell_class
-class NewslettersCell(CellBase):
-    title = models.CharField(verbose_name=_('Title'), max_length=128)
-    url = models.URLField(verbose_name=_('Newsletters service url'), max_length=128)
-    resources_restrictions = models.CharField(
-        _('resources restrictions'),
-        blank=True,
-        max_length=1024,
-        help_text=_('list of resources(themes) separated by commas'),
-    )
-    transports_restrictions = models.CharField(
-        _('transports restrictions'),
-        blank=True,
-        max_length=1024,
-        help_text=_('list of transports separated by commas'),
-    )
-
-    template_name = 'newsletters/newsletters.html'
-    user_dependant = True
-
-    @classmethod
-    def is_enabled(cls):
-        return settings.NEWSLETTERS_CELL_ENABLED
-
-    class Meta:
-        verbose_name = _('Newsletters')
-
-    def get_default_form_class(self):
-        model_fields = ('title', 'url', 'resources_restrictions', 'transports_restrictions')
-        return model_forms.modelform_factory(self.__class__, fields=model_fields)
-
-    def simplify(self, name):
-        return slugify(name.strip())
-
-    def get_resources_restrictions(self):
-        return list(filter(None, map(self.simplify, self.resources_restrictions.strip().split(','))))
-
-    def get_transports_restrictions(self):
-        return list(filter(None, map(self.simplify, self.transports_restrictions.strip().split(','))))
-
-    def check_resource(self, resource):
-        restrictions = self.get_resources_restrictions()
-        if restrictions and self.simplify(resource) not in restrictions:
-            return False
-        return True
-
-    def check_transport(self, transport):
-        restrictions = self.get_transports_restrictions()
-        if restrictions and transport not in restrictions:
-            return False
-        return True
-
-    def filter_data(self, data):
-        filtered = []
-        for item in data:
-            if not self.check_resource(item['text']):
-                continue
-            for t in item['transports']:
-                if self.check_transport(t['id']):
-                    filtered.append(item)
-        return filtered
-
-    def get_newsletters(self):
-        endpoint = self.url + 'newsletters/'
-        try:
-            response = requests.get(endpoint, remote_service='auto', cache_duration=60, without_user=True)
-            response.raise_for_status()
-        except RequestException:
-            return []
-        json_response = response.json()
-        return self.filter_data(json_response['data'])
-
-    def get_subscriptions(self, user, **kwargs):
-        endpoint = self.url + 'subscriptions/'
-        try:
-            response = requests.get(
-                endpoint, remote_service='auto', user=user, cache_duration=0, params=kwargs
-            )
-            response.raise_for_status()
-        except RequestException:
-            return []
-        json_response = response.json()
-        return self.filter_data(json_response['data'])
-
-    def set_subscriptions(self, subscriptions, user, **kwargs):
-        logger = logging.getLogger(__name__)
-        # uuid is mandatory to store subscriptions
-        if 'uuid' not in kwargs:
-            raise SubscriptionsSaveError
-        headers = {'Content-type': 'application/json', 'Accept': 'application/json'}
-        endpoint = self.url + 'subscriptions/'
-        try:
-            response = requests.post(
-                endpoint,
-                remote_service='auto',
-                data=json.dumps(subscriptions),
-                user=user,
-                federation_key='email',
-                params=kwargs,
-                headers=headers,
-            )
-            response.raise_for_status()
-            if not response.json()['data']:
-                raise SubscriptionsSaveError
-        except HTTPError as e:
-            logger.error(
-                u'set subscriptions on %s returned an HTTP error code: %s',
-                e.response.request.url,
-                e.response.status_code,
-            )
-            raise SubscriptionsSaveError
-        except RequestException as e:
-            logger.error(u'set subscriptions on %s failed with exception: %s', endpoint, e)
-            raise SubscriptionsSaveError
-
-    def render(self, context):
-        user = context.get('user')
-        if user and user.is_authenticated:
-            form = NewslettersManageForm(instance=self, request=context['request'])
-            context['form'] = form
-        return super(NewslettersCell, self).render(context)
-
-    def is_visible(self, **kwargs):
-        user = kwargs.get('user')
-        if user is None or not user.is_authenticated:
-            return False
-        return super(NewslettersCell, self).is_visible(**kwargs)
diff --git a/combo/apps/newsletters/templates/newsletters/newsletters.html b/combo/apps/newsletters/templates/newsletters/newsletters.html
deleted file mode 100644
index 765f7fcc..00000000
--- a/combo/apps/newsletters/templates/newsletters/newsletters.html
+++ /dev/null
@@ -1,43 +0,0 @@
-{% load i18n %}
-<h2>{{ cell.title }}</h2>
-{% if form %}
-{% if form.non_field_errors %}
-{{ form.non_field_errors }}
-{% else %}
-<form method="post" action={% url 'newsletters-update' pk=cell.pk %}>
-  {% csrf_token %}
-
-  {% for field in form %}
-  {{ field.error }}
-  {% endfor %}
-  <table class="newsletters-form">
-    <thead>
-      <tr>
-        <td>{% trans "Theme" %}</td>
-        {% for id, theme in form.themes %}
-        <td data-id="{{ id }}">{{ theme }}</td>
-        {% endfor %}
-      </tr>
-    </thead>
-    <tbody>
-      {% for field in form %}
-      <tr>
-        <td>{{ field.label }}</td>
-        {% for id, theme in form.themes %}<td data-id="{{ id }}">
-        {% for w in field %}
-          {% with choice_value=w.data.value choice_value18=w.choice_value %}
-            {% if choice_value == id or choice_value18 == id %}{{ w }}{% endif %}
-          {% endwith %}
-        {% endfor %}
-        </td>
-        {% endfor %}
-      </tr>
-      {% endfor %}
-    </tbody>
-  </table>
-  <button>{% trans "Modify" %}</button>
-</form>
-{% endif %}
-{% else %}
-<div>{% trans "No subscriptions" %}</div>
-{% endif %}
diff --git a/combo/apps/newsletters/urls.py b/combo/apps/newsletters/urls.py
deleted file mode 100644
index c53a0265..00000000
--- a/combo/apps/newsletters/urls.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from django.conf.urls import url
-from django.contrib.auth.decorators import login_required
-
-from .views import NewslettersView
-
-urlpatterns = [
-    url(
-        r'^newsletters/(?P<pk>\w+)/update$',
-        login_required(NewslettersView.as_view()),
-        name='newsletters-update',
-    ),
-]
diff --git a/combo/apps/newsletters/views.py b/combo/apps/newsletters/views.py
deleted file mode 100644
index 0e333065..00000000
--- a/combo/apps/newsletters/views.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# combo - content management system
-# Copyright (C) 2015  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.contrib import messages
-from django.http import HttpResponseRedirect
-from django.utils.translation import ugettext_lazy as _
-from django.views.generic import FormView
-
-from .forms import NewslettersManageForm
-from .models import NewslettersCell, SubscriptionsSaveError
-
-
-class NewslettersView(FormView):
-    http_method_names = ['post']
-    form_class = NewslettersManageForm
-
-    def form_valid(self, form):
-        try:
-            form.save()
-            messages.info(self.request, _('Your subscriptions are successfully saved'))
-        except SubscriptionsSaveError:
-            messages.error(
-                self.request, _('An error occured while saving your subscriptions. Please try later.')
-            )
-        return super(NewslettersView, self).form_valid(form)
-
-    def get_form_kwargs(self):
-        kwargs = super(NewslettersView, self).get_form_kwargs()
-        self.instance = NewslettersCell.objects.get(pk=self.kwargs['pk'])
-        kwargs.update({'request': self.request, 'instance': self.instance})
-        return kwargs
-
-    def form_invalid(self, form):
-        messages.error(self.request, _('An error occured while saving your subscriptions. Please try later.'))
-        return HttpResponseRedirect(self.get_success_url())
-
-    def get_success_url(self):
-        return self.instance.page.get_online_url()
diff --git a/combo/settings.py b/combo/settings.py
index 5b0311a4..9cc1df9a 100644
--- a/combo/settings.py
+++ b/combo/settings.py
@@ -70,7 +70,6 @@ INSTALLED_APPS = (
     'combo.apps.family',
     'combo.apps.dataviz',
     'combo.apps.lingo',
-    'combo.apps.newsletters',
     'combo.apps.fargo',
     'combo.apps.notifications',
     'combo.apps.search',
@@ -363,7 +362,6 @@ PWA_NOTIFICATION_ICON_URL = None
 # hide work-in-progress/experimental/broken/legacy/whatever cells for now
 BOOKING_CALENDAR_CELL_ENABLED = False
 LEGACY_CHART_CELL_ENABLED = False
-NEWSLETTERS_CELL_ENABLED = False
 
 
 def debug_show_toolbar(request):
diff --git a/tests/settings.py b/tests/settings.py
index 5a808e1a..5950637b 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -82,7 +82,6 @@ FAMILY_SERVICE = {'root': '/'}
 
 BOOKING_CALENDAR_CELL_ENABLED = True
 LEGACY_CHART_CELL_ENABLED = True
-NEWSLETTERS_CELL_ENABLED = True
 
 USER_PROFILE_CONFIG = {
     'fields': [
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 4dba7042..124668ad 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -862,7 +862,7 @@ def test_site_export_import_json(app, admin_user):
     resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
     with CaptureQueriesContext(connection) as ctx:
         resp = resp.form.submit()
-        assert len(ctx.captured_queries) in [303, 304]
+        assert len(ctx.captured_queries) in [298, 299]
     assert Page.objects.count() == 4
     assert PageSnapshot.objects.all().count() == 4
 
@@ -873,7 +873,7 @@ def test_site_export_import_json(app, admin_user):
     resp.form['site_file'] = Upload('site-export.json', site_export, 'application/json')
     with CaptureQueriesContext(connection) as ctx:
         resp = resp.form.submit()
-        assert len(ctx.captured_queries) == 272
+        assert len(ctx.captured_queries) == 268
     assert set(Page.objects.get(slug='one').related_cells['cell_types']) == set(
         ['data_textcell', 'data_linkcell']
     )
@@ -2178,7 +2178,7 @@ def test_page_versionning(app, admin_user):
 
     with CaptureQueriesContext(connection) as ctx:
         resp2 = resp.click('view', index=1)
-        assert len(ctx.captured_queries) == 71
+        assert len(ctx.captured_queries) == 70
     assert Page.snapshots.latest('pk').related_cells == {'cell_types': ['data_textcell']}
     assert resp2.text.index('Hello world') < resp2.text.index('Foobar3')
 
@@ -2239,7 +2239,7 @@ def test_page_versionning(app, admin_user):
     resp = resp.click('restore', index=6)
     with CaptureQueriesContext(connection) as ctx:
         resp = resp.form.submit().follow()
-        assert len(ctx.captured_queries) == 144
+        assert len(ctx.captured_queries) == 142
 
     resp2 = resp.click('See online')
     assert resp2.text.index('Foobar1') < resp2.text.index('Foobar2') < resp2.text.index('Foobar3')
diff --git a/tests/test_newsletters_cell.py b/tests/test_newsletters_cell.py
deleted file mode 100644
index 26631621..00000000
--- a/tests/test_newsletters_cell.py
+++ /dev/null
@@ -1,275 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import json
-
-import mock
-import pytest
-import requests
-from django.contrib.auth.models import User
-from django.template.defaultfilters import slugify
-
-from combo.apps.newsletters.forms import NewslettersManageForm
-from combo.apps.newsletters.models import NewslettersCell, SubscriptionsSaveError
-from combo.data.models import Page
-from combo.utils import check_query
-
-pytestmark = pytest.mark.django_db
-
-NEWSLETTERS = [
-    {'id': '1', 'text': 'Democratie locale', 'transports': [{'id': 'mail', 'text': 'mail'}]},
-    {'id': '2', 'text': 'Rencontres de quartiers', 'transports': [{'id': 'mail', 'text': 'mail'}]},
-    {
-        'id': '3',
-        'text': 'Environnement',
-        'transports': [
-            {'id': 'mail', 'text': 'mail'},
-            {'id': 'sms', 'text': 'sms'},
-            {'id': 'rss', 'text': 'rss'},
-        ],
-    },
-    {
-        'id': '4',
-        'text': u'Marchés publics',
-        'transports': [{'id': 'mail', 'text': 'mail'}, {'id': 'rss', 'text': 'rss'}],
-    },
-    {
-        'id': '5',
-        'text': "Offres d'emploi",
-        'transports': [{'id': 'mail', 'text': 'mail'}, {'id': 'rss', 'text': 'rss'}],
-    },
-    {
-        'id': '6',
-        'text': 'Infos créche',
-        'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'rss', 'text': 'rss'}],
-    },
-    {
-        'id': '7',
-        'text': 'Familles',
-        'transports': [{'id': 'mail', 'text': 'mail'}, {'id': 'sms', 'text': 'sms'}],
-    },
-    {
-        'id': '8',
-        'text': 'Travaux',
-        'transports': [
-            {'id': 'mail', 'text': 'mail'},
-            {'id': 'sms', 'text': 'sms'},
-            {'id': 'rss', 'text': 'rss'},
-        ],
-    },
-]
-
-SUBSCRIPTIONS = [
-    {'id': '3', 'text': 'Environnement', 'transports': [{'id': 'mail', 'text': 'mail'}]},
-    {'id': '7', 'text': 'Familles', 'transports': [{'id': 'mail', 'text': 'mail'}]},
-    {'id': '5', 'text': "Offres d'emploi", 'transports': [{'id': 'mail', 'text': 'mail'}]},
-    {
-        'id': '6',
-        'text': 'Infos créche',
-        'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'rss', 'text': 'rss'}],
-    },
-]
-
-USER_EMAIL = 'foobar@example.com'
-
-
-@pytest.fixture
-def cell():
-    page = Page()
-    page.save()
-    cell = NewslettersCell(title='Newsletter test', url='http://example.org/', page=page, order=0)
-    cell.save()
-    return cell
-
-
-@pytest.fixture
-def user():
-    try:
-        user = User.objects.get(username='foo')
-    except User.DoesNotExist:
-        user = User.objects.create(username='foo', email=USER_EMAIL)
-    return user
-
-
-@mock.patch('combo.apps.newsletters.models.requests.get')
-def test_get_newsletters_by_transports(mock_get, cell):
-    restrictions = ('mail', 'sms')
-    cell.transports_restrictions = ','.join(restrictions)
-    expected_newsletters = []
-    for n in NEWSLETTERS:
-        for t in n['transports']:
-            if t['id'] in restrictions:
-                expected_newsletters.append(n)
-                continue
-    mock_json = mock.Mock()
-    mock_json.json.return_value = {'err': 0, 'data': NEWSLETTERS}
-    mock_get.return_value = mock_json
-    assert cell.get_newsletters() == expected_newsletters
-    assert mock_get.call_args[1]['without_user']
-    assert 'user' not in mock_get.call_args[1]
-
-    mock_get.side_effect = requests.RequestException
-    assert cell.get_newsletters() == []
-
-
-@mock.patch('combo.apps.newsletters.models.requests.get')
-def test_get_newsletters_by_unrestricted_transports(mock_get, cell):
-    cell.transports_restrictions = ''
-    expected_newsletters = []
-    for n in NEWSLETTERS:
-        for t in n['transports']:
-            expected_newsletters.append(n)
-    mock_json = mock.Mock()
-    mock_json.json.return_value = {'err': 0, 'data': NEWSLETTERS}
-    mock_get.return_value = mock_json
-    assert cell.get_newsletters() == expected_newsletters
-
-
-@mock.patch('combo.apps.newsletters.models.requests.get')
-def test_get_newsletters_by_resources(mock_get, cell):
-    restrictions = ('marches-publics', 'familles', 'democratie-locale')
-    cell.transports_restrictions = 'mail'
-    cell.resources_restrictions = ','.join(restrictions)
-    expected_newsletters = []
-    for n in NEWSLETTERS:
-        if slugify(n['text']) in restrictions:
-            expected_newsletters.append(n)
-    mock_json = mock.Mock()
-    mock_json.json.return_value = {'err': 0, 'data': NEWSLETTERS}
-    mock_get.return_value = mock_json
-    assert cell.get_newsletters() == expected_newsletters
-
-
-@mock.patch('combo.apps.newsletters.models.requests.get')
-def test_get_subscriptions(mock_get, cell, user):
-    restrictions = ('mail', 'sms')
-    cell.transports_restrictions = ','.join(restrictions)
-    expected_subscriptions = []
-    for n in SUBSCRIPTIONS:
-        for t in n['transports']:
-            if t['id'] in restrictions:
-                expected_subscriptions.append(n)
-                continue
-    mock_json = mock.Mock()
-    mock_json.json.return_value = {'err': 0, 'data': SUBSCRIPTIONS}
-    mock_get.return_value = mock_json
-    assert cell.get_subscriptions(user) == expected_subscriptions
-    assert mock_get.call_args[1]['user'].email == USER_EMAIL
-
-    mock_get.side_effect = requests.RequestException
-    assert cell.get_subscriptions(user) == []
-
-
-@mock.patch('combo.utils.requests_wrapper.RequestsSession.send')
-def test_get_subscriptions_signature_check(mock_send, cell, user):
-    restrictions = ('mail', 'sms')
-    cell.transports_restrictions = ','.join(restrictions)
-    expected_subscriptions = []
-    for n in SUBSCRIPTIONS:
-        for t in n['transports']:
-            if t['id'] in restrictions:
-                expected_subscriptions.append(n)
-                continue
-    mock_json = mock.Mock(status_code=200)
-    mock_json.json.return_value = {'err': 0, 'data': SUBSCRIPTIONS}
-    mock_send.return_value = mock_json
-    cell.get_subscriptions(user)
-    url = mock_send.call_args[0][0].url
-    assert check_query(url.split('?', 1)[-1], 'combo')
-
-
-@mock.patch('combo.apps.newsletters.models.requests.post')
-def test_failed_set_subscriptions(mock_post, cell, user):
-    restrictions = ('sms', 'mail')
-    cell.transports_restrictions = ','.join(restrictions)
-    subscriptions = [
-        {'id': '1', 'transports': [{'id': 'mail', 'text': 'mail'}]},
-        {'id': '7', 'transports': [{'id': 'sms', 'text': 'sms'}]},
-        {'id': '8', 'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'mail', 'text': 'mail'}]},
-    ]
-    mock_post.side_effect = requests.ConnectionError
-    with pytest.raises(SubscriptionsSaveError):
-        cell.set_subscriptions(subscriptions, user, uuid='useruuid')
-
-    mock_post.side_effect = requests.HTTPError(response=mock.MagicMock())
-    with pytest.raises(SubscriptionsSaveError):
-        cell.set_subscriptions(subscriptions, user, uuid='useruuid')
-
-
-def test_set_subscriptions_with_no_uuid(cell, user):
-    restrictions = ('sms', 'mail')
-    cell.transports_restrictions = ','.join(restrictions)
-    subscriptions = [
-        {'id': '1', 'transports': [{'id': 'mail', 'text': 'mail'}]},
-        {'id': '8', 'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'mail', 'text': 'mail'}]},
-    ]
-    with pytest.raises(SubscriptionsSaveError):
-        cell.set_subscriptions(subscriptions, user)
-
-
-@mock.patch('combo.apps.newsletters.models.requests.post')
-def test_set_subscriptions(mock_post, cell, user):
-    restrictions = ('sms', 'mail')
-    cell.transports_restrictions = ','.join(restrictions)
-    subscriptions = [
-        {'id': '1', 'transports': [{'id': 'mail', 'text': 'mail'}]},
-        {'id': '7', 'transports': [{'id': 'sms', 'text': 'sms'}]},
-        {'id': '8', 'transports': [{'id': 'sms', 'text': 'sms'}, {'id': 'mail', 'text': 'mail'}]},
-    ]
-    mock_json = mock.Mock()
-    mock_json.json.return_value = {'err': 0, 'data': True}
-    mock_post.return_value = mock_json
-    cell.set_subscriptions(subscriptions, user, uuid='useruuid')
-    args, kwargs = mock_post.call_args
-    assert kwargs['params']['uuid'] == 'useruuid'
-    assert kwargs['federation_key'] == 'email'
-    assert kwargs['user'].email == USER_EMAIL
-
-
-@mock.patch('combo.apps.newsletters.models.requests.get')
-def test_get_subscriptions_with_name_id_and_mobile(mock_get, cell, user):
-    restrictions = ('sms', 'mail')
-    cell.transports_restrictions = ','.join(restrictions)
-    mock_json = mock.Mock()
-    mock_json.json.return_value = {'err': 0, 'data': NEWSLETTERS}
-    mock_get.return_value = mock_json
-
-    fake_saml_request = mock.Mock()
-    fake_saml_request.user = mock.Mock(email=USER_EMAIL)
-    fake_saml_request.user.get_name_id.return_value = 'nameid'
-    fake_saml_request.session = {'mellon_session': {'mobile': '0607080900'}}
-
-    form = NewslettersManageForm(instance=cell, request=fake_saml_request)
-    args, kwargs = mock_get.call_args
-    assert kwargs['params'] == {'uuid': 'nameid', 'mobile': '0607080900'}
-
-
-def mocked_requests_get(*args, **kwargs):
-    url = args[0]
-
-    class MockResponse(mock.Mock):
-        status_code = 200
-
-        def json(self):
-            return json.loads(self.content)
-
-    if 'newsletters' in url:
-        return MockResponse(content=json.dumps({'data': NEWSLETTERS}))
-    else:
-        return MockResponse(content=json.dumps({'data': SUBSCRIPTIONS}))
-
-
-@mock.patch('combo.apps.newsletters.models.requests.get', side_effect=mocked_requests_get)
-def test_subscriptions_form(mock_get, cell, user):
-    restrictions = ('sms', 'mail')
-    cell.transports_restrictions = ','.join(restrictions)
-    newsletters = [n['id'] for n in cell.get_newsletters()]
-    assert mock_get.call_args[1]['without_user']
-    subscriptions = [s['id'] for s in cell.get_subscriptions(user)]
-    fake_request = mock.Mock(user=mock.Mock(username='username', email=USER_EMAIL), session={})
-    form = NewslettersManageForm(instance=cell, request=fake_request)
-    # test if all newsletters are present
-    for f_id, field in form.fields.items():
-        assert f_id in newsletters
-    # test if initial fields are in the restrictions
-    for s in subscriptions:
-        assert set(form.fields[s].initial).intersection(set(restrictions))
diff --git a/tests/test_search.py b/tests/test_search.py
index e6fa8d15..ca842494 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -1364,7 +1364,7 @@ def test_index_site_num_queries(settings, app):
     assert IndexedCell.objects.count() == 50
     with CaptureQueriesContext(connection) as ctx:
         index_site()
-        assert len(ctx.captured_queries) == 224
+        assert len(ctx.captured_queries) == 223
 
     SearchCell.objects.create(
         page=page, placeholder='content', order=0, _search_services={'data': ['search1']}
-- 
2.30.2

