Commit c772f1b4 authored by Luko van der Maas's avatar Luko van der Maas

Merge branch 'groups-syncing' into 'master'

Sync mailinglists with GSuite Groups

Closes #876 and #935

See merge request !1379
parents 6dd4e481 27d8e4e3
......@@ -32,6 +32,14 @@ mailinglists.apps module
:undoc-members:
:show-inheritance:
mailinglists.gsuite module
--------------------------
.. automodule:: mailinglists.gsuite
:members:
:undoc-members:
:show-inheritance:
mailinglists.models module
--------------------------
......@@ -48,3 +56,11 @@ mailinglists.services module
:undoc-members:
:show-inheritance:
mailinglists.signals module
---------------------------
.. automodule:: mailinglists.signals
:members:
:undoc-members:
:show-inheritance:
......@@ -43,6 +43,14 @@ utils.exception\_filter module
:undoc-members:
:show-inheritance:
utils.google\_api module
------------------------
.. automodule:: utils.google_api
:members:
:undoc-members:
:show-inheritance:
utils.snippets module
---------------------
......
This diff is collapsed.
......@@ -31,6 +31,7 @@ django-sendfile2 = "~0.4.2"
# docs requirements
recommonmark = { version = "~0.6.0", optional = true }
sphinx = { version = "~2.2", optional = true }
google-api-python-client = "^1.7.11"
[tool.poetry.extras]
docs = ["recommonmark", "sphinx"]
......
import datetime
import factory
from django.core.exceptions import ValidationError
from django.db.models import signals
from django.test import TestCase
from django.utils import timezone
......@@ -16,6 +18,7 @@ class EventTest(TestCase):
fixtures = ['members.json']
@classmethod
@factory.django.mute_signals(signals.pre_save)
def setUpTestData(cls):
cls.mailinglist = MailingList.objects.create(
name="testmail"
......
import datetime
import factory
from django.contrib.auth.models import Permission
from django.core import mail
from django.db.models import signals
from django.test import Client, TestCase
from django.utils import timezone
......@@ -134,6 +136,7 @@ class RegistrationTest(TestCase):
fixtures = ['members.json', 'member_groups.json']
@classmethod
@factory.django.mute_signals(signals.pre_save)
def setUpTestData(cls):
cls.mailinglist = MailingList.objects.create(
name="testmail"
......
......@@ -28,5 +28,5 @@ class MailingListAdmin(admin.ModelAdmin):
def alias_names(self, obj):
"""Return list of aliases of obj."""
return [x.alias for x in obj.aliasses.all()]
alias_names.short_description = _('List aliasses')
return [x.alias for x in obj.aliases.all()]
alias_names.short_description = _('List aliases')
......@@ -19,7 +19,7 @@ class MailingListSerializer(serializers.ModelSerializer):
def _names(self, instance):
"""Return list of names of the the mailing list and its aliases."""
return [instance.name] + [x.alias for x in instance.aliasses.all()]
return [instance.name] + [x.alias for x in instance.aliases.all()]
def _addresses(self, instance):
"""Return list of all subscribed addresses."""
......
......@@ -8,3 +8,7 @@ class MailinglistsConfig(AppConfig):
name = 'mailinglists'
verbose_name = _('Mailing lists')
def ready(self):
"""Imports the signals when the app is ready"""
from . import signals # noqa: F401
This diff is collapsed.
"""Mailing list syncing management command"""
import logging
from django.core.management.base import BaseCommand
from mailinglists.gsuite import GSuiteSyncService
logger = logging.getLogger(__name__)
class Command(BaseCommand):
def handle(self, *args, **options):
"""Sync all mailing lists"""
sync_service = GSuiteSyncService()
sync_service.sync_mailinglists()
# Generated by Django 2.2.1 on 2019-10-13 21:52
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mailinglists', '0014_mailinglist_description'),
]
operations = [
migrations.AlterModelOptions(
name='listalias',
options={'verbose_name': 'List alias', 'verbose_name_plural': 'List aliases'},
),
migrations.RemoveField(
model_name='mailinglist',
name='autoresponse_enabled',
),
migrations.RemoveField(
model_name='mailinglist',
name='autoresponse_text',
),
migrations.RemoveField(
model_name='mailinglist',
name='prefix',
),
migrations.RemoveField(
model_name='mailinglist',
name='archived',
),
migrations.AlterField(
model_name='listalias',
name='mailinglist',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='mailinglists.MailingList', verbose_name='Mailing list'),
),
]
......@@ -41,25 +41,11 @@ class MailingList(models.Model):
help_text=_('Enter the name for the list (i.e. name@thalia.nu).'),
)
prefix = models.CharField(
verbose_name=_("Prefix"),
blank=True,
max_length=200,
help_text=_('Enter a prefix that should be prefixed to subjects '
'of all emails sent via this mailinglist.'),
)
description = models.TextField(
verbose_name=_("Description"),
help_text=_('Write a description for the mailinglist.'),
)
archived = models.BooleanField(
verbose_name=_("Archived"),
default=True,
help_text=_('Indicate whether an archive should be kept.')
)
moderated = models.BooleanField(
verbose_name=_("Moderated"),
default=False,
......@@ -80,18 +66,6 @@ class MailingList(models.Model):
blank=True,
)
autoresponse_enabled = models.BooleanField(
verbose_name=_("Automatic response enabled"),
default=False,
help_text=_('Indicate whether emails will get an automatic response.')
)
autoresponse_text = models.TextField(
verbose_name=_("Autoresponse text"),
null=True,
blank=True,
)
def all_addresses(self):
"""Return all addresses subscribed to this mailing list."""
for member in self.members.all():
......@@ -119,11 +93,6 @@ class MailingList(models.Model):
}
})
if not self.autoresponse_text and self.autoresponse_enabled:
raise ValidationError({
'autoresponse_text': _('Enter a text for the auto response.')
})
def __str__(self):
"""Return the name of the mailing list."""
return self.name
......@@ -169,7 +138,7 @@ class ListAlias(models.Model):
mailinglist = models.ForeignKey(MailingList,
verbose_name=_("Mailing list"),
on_delete=models.CASCADE,
related_name='aliasses')
related_name='aliases')
def clean(self):
"""Validate the alias."""
......@@ -195,4 +164,4 @@ class ListAlias(models.Model):
"""Meta class for ListAlias."""
verbose_name = _("List alias")
verbose_name_plural = _("List aliasses")
verbose_name_plural = _("List aliases")
"""The services defined by the mailinglists package"""
from django.conf import settings
from django.utils import timezone
......@@ -39,37 +40,36 @@ def get_automatic_lists():
if 0 < timezone.now().month < 9:
lectureyear += 1
active_mentorships = Mentorship.objects.filter(
year=lectureyear)
year=lectureyear).prefetch_related('member')
mentors = [x.member for x in active_mentorships]
lists = []
lists += _create_automatic_list(
['leden', 'members'], '[THALIA]',
Member.all_with_membership('member'), True, True, True)
['members', 'leden'], '[THALIA]',
Member.all_with_membership('member'), '', True, True, True)
lists += _create_automatic_list(
['begunstigers', 'benefactors'],
['benefactors', 'begunstigers'],
'[THALIA]',
Member.all_with_membership(Membership.BENEFACTOR),
Member.all_with_membership(Membership.BENEFACTOR), '',
multilingual=True)
lists += _create_automatic_list(
['ereleden', 'honorary'], '[THALIA]', Member.all_with_membership(
'honorary'), multilingual=True)
['honorary', 'ereleden'], '[THALIA]', Member.all_with_membership(
'honorary'), '', multilingual=True)
lists += _create_automatic_list(
['mentors'], '[THALIA] [MENTORS]', mentors, moderated=False)
['mentors'], '[THALIA] [MENTORS]', mentors, '', moderated=False)
lists += _create_automatic_list(
['activemembers'], '[THALIA] [COMMITTEES]',
active_members)
active_members, '')
lists += _create_automatic_list(
['commissievoorzitters', 'committeechairs'], '[THALIA] [CHAIRS]',
committee_chair_emails, moderated=False)
['committeechairs', 'commissievoorzitters'], '[THALIA] [CHAIRS]',
committee_chair_emails, '', moderated=False)
lists += _create_automatic_list(
['gezelschapvoorzitters', 'societychairs'], '[THALIA] [SOCIETY]',
society_chair_emails, moderated=False)
['societychairs', 'gezelschapvoorzitters'], '[THALIA] [SOCIETY]',
society_chair_emails, '', moderated=False)
lists += _create_automatic_list(
['optin'], '[THALIA] [OPTIN]', Member.current_members.filter(
profile__receive_optin=True),
multilingual=True)
profile__receive_optin=True), '', multilingual=True)
all_previous_board_members = []
......@@ -97,10 +97,13 @@ def get_automatic_lists():
return lists
def _create_automatic_list(names, prefix, members,
def _create_automatic_list(names, prefix, members, description='',
archived=True, moderated=True, multilingual=False):
data = {
'names': names,
'name': names[0],
'description': description,
'aliases': names[1:],
'prefix': prefix,
'archived': archived,
'moderated': moderated,
......@@ -116,6 +119,8 @@ def _create_automatic_list(names, prefix, members,
if member.profile.language == language[0]]
localized_data['names'] = [
'{}-{}'.format(n, language[0]) for n in names]
localized_data['name'] = localized_data['names'][0]
localized_data['aliases'] = localized_data['names'][1:]
yield localized_data # these are localized lists, e.g. leden-nl@
else:
data['addresses'] = set([member.email for member in members])
......
from django.db.models.signals import pre_save
from django.dispatch import receiver
from googleapiclient.errors import HttpError
from mailinglists.gsuite import GSuiteSyncService
from mailinglists.models import MailingList
@receiver(pre_save, sender='mailinglists.MailingList')
def pre_mailinglist_save(instance, **kwargs):
sync_service = GSuiteSyncService()
group = sync_service.mailinglist_to_group(instance)
old_list = MailingList.objects.filter(pk=instance.pk).first()
try:
if old_list is None:
sync_service.create_group(group)
else:
sync_service.update_group(old_list.name, group)
except HttpError:
# Cannot do direct create or update, do full sync for list
sync_service.sync_mailinglists([group])
This diff is collapsed.
"""Tests for models in the mailinglists package"""
import factory
from django.core.exceptions import ValidationError
from django.db.models import signals
from django.test import TestCase
from mailinglists.models import MailingList, ListAlias
......@@ -9,6 +11,7 @@ class MailingListTest(TestCase):
"""Tests mailing lists"""
@classmethod
@factory.django.mute_signals(signals.pre_save)
def setUpTestData(cls):
cls.mailinglist = MailingList.objects.create(
name="mailtest",
......@@ -43,20 +46,12 @@ class MailingListTest(TestCase):
mailinglist.name = "activemembers1"
mailinglist.clean()
def test_autoresponse_has_text(self):
self.mailinglist.autoresponse_enabled = True
with self.assertRaises(ValidationError):
self.mailinglist.clean()
self.mailinglist.autoresponse_text = "Hello World"
self.mailinglist.clean()
class ListAliasTest(TestCase):
"""Tests list aliases"""
@classmethod
@factory.django.mute_signals(signals.pre_save)
def setUpTestData(cls):
cls.mailinglist = MailingList.objects.create(
name="mailtest",
......
......@@ -9,6 +9,8 @@ overrides.
# flake8: noqa: ignore F403
import logging
from firebase_admin import initialize_app, credentials
from googleapiclient.discovery import build
from google.oauth2 import service_account
# Load all default settings because we need to use settings.configure
# for sphinx documentation generation.
......@@ -39,3 +41,11 @@ if FIREBASE_CREDENTIALS != {}:
credential=credentials.Certificate(FIREBASE_CREDENTIALS))
except ValueError as e:
logger.error('Firebase application failed to initialise')
if GSUITE_ADMIN_CREDENTIALS != {}:
GSUITE_ADMIN_CREDENTIALS = (
service_account.Credentials.from_service_account_info(
GSUITE_ADMIN_CREDENTIALS, scopes=GSUITE_ADMIN_SCOPES
).with_subject(GSUITE_ADMIN_USER)
)
......@@ -78,6 +78,14 @@ if not (FIREBASE_CREDENTIALS == '{}'):
FIREBASE_CREDENTIALS = base64.urlsafe_b64decode(FIREBASE_CREDENTIALS)
FIREBASE_CREDENTIALS = json.loads(FIREBASE_CREDENTIALS)
GSUITE_ADMIN_CREDENTIALS = os.environ.get('GSUITE_ADMIN_CREDENTIALS', '{}')
if not (GSUITE_ADMIN_CREDENTIALS == '{}'):
GSUITE_ADMIN_CREDENTIALS = base64.urlsafe_b64decode(
GSUITE_ADMIN_CREDENTIALS)
GSUITE_ADMIN_CREDENTIALS = json.loads(GSUITE_ADMIN_CREDENTIALS)
GSUITE_ADMIN_USER = os.environ.get('GSUITE_ADMIN_USER', None)
GSUITE_DOMAIN = os.environ.get('GSUITE_DOMAIN', 'thalia.nu')
if os.environ.get('DJANGO_SSLONLY'):
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
......
......@@ -261,6 +261,16 @@ THUMBNAIL_SIZES = {
# Placeholder Firebase config
FIREBASE_CREDENTIALS = {}
# Placeholder GSuite config
GSUITE_ADMIN_CREDENTIALS = {}
GSUITE_ADMIN_USER = 'concrexit@thalia.nu'
GSUITE_ADMIN_SCOPES = [
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/apps.groups.settings'
]
GSUITE_DOMAIN = 'thalia.localhost'
# Default FROM email
DEFAULT_FROM_EMAIL = f'noreply@{SITE_DOMAIN}'
SERVER_EMAIL = DEFAULT_FROM_EMAIL
......
from googleapiclient.discovery import build
from googleapiclient.discovery_cache.base import Cache
from thaliawebsite import settings
class MemoryCache(Cache):
_CACHE = {}
def get(self, url):
return MemoryCache._CACHE.get(url)
def set(self, url, content):
MemoryCache._CACHE[url] = content
memory_cache = MemoryCache()
def get_directory_api():
return build(
'admin', 'directory_v1',
credentials=settings.GSUITE_ADMIN_CREDENTIALS,
cache=memory_cache
)
def get_groups_settings_api():
return build(
'groupssettings', 'v1',
credentials=settings.GSUITE_ADMIN_CREDENTIALS,
cache=memory_cache
)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment