Send invitation emails with OpenSlides login.

This commit is contained in:
FinnStutzenstein 2017-11-28 10:47:29 +01:00 committed by Emanuel Schütze
parent a34ad1485a
commit 2220112d27
22 changed files with 307 additions and 18 deletions

View File

@ -71,6 +71,7 @@ Users:
default is now disabled [#3400]. default is now disabled [#3400].
- Hide password in change password view [#3417]. - Hide password in change password view [#3417].
- Added a change presence view [#3496]. - Added a change presence view [#3496].
- New feature to send invitation emails with OpenSlides login [#3503].
Core: Core:
- No reload on logoff. OpenSlides is now a full single page - No reload on logoff. OpenSlides is now a full single page

View File

@ -159,7 +159,7 @@
<div class="col-xs-11 main-header"> <div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right"> <span class="form-inline text-right pull-right">
<!-- clear all filters --> <!-- clear all filters -->
<span class="sort-spacer pointer" ng-click="filter.reset()" <span class="sort-spacer pointer" ng-click="filter.reset(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode" ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}"> ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>

View File

@ -93,7 +93,7 @@
<div class="col-xs-11 main-header"> <div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right"> <span class="form-inline text-right pull-right">
<!-- clear all filters --> <!-- clear all filters -->
<span class="sort-spacer pointer" ng-click="filter.reset()" <span class="sort-spacer pointer" ng-click="filter.reset(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode" ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}"> ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>

View File

@ -709,8 +709,9 @@ angular.module('OpenSlidesApp.core', [
message += gettextCatalog.getString("The server didn't respond."); message += gettextCatalog.getString("The server didn't respond.");
} else if (error.data.detail) { } else if (error.data.detail) {
message += error.data.detail; message += error.data.detail;
} else if (error.status === 500) { } else if (error.status > 500) { // Some kind of server error.
message += gettextCatalog.getString("A server error occurred. Please check the system logs."); message += gettextCatalog.getString("A server error occurred (%%code%%). Please check the system logs.");
message = message.replace('%%code%%', error.status);
} else { } else {
for (var e in error.data) { for (var e in error.data) {
message += e + ': ' + error.data[e] + ' '; message += e + ': ' + error.data[e] + ' ';

View File

@ -535,7 +535,10 @@ angular.module('OpenSlidesApp.core.site', [
areFiltersSet = areFiltersSet || (self.filterString !== ''); areFiltersSet = areFiltersSet || (self.filterString !== '');
return areFiltersSet !== false; return areFiltersSet !== false;
}; };
self.reset = function () { self.reset = function (danger) {
if (danger) {
return;
}
_.forEach(self.multiselectFilters, function (filterList, filter) { _.forEach(self.multiselectFilters, function (filterList, filter) {
self.multiselectFilters[filter] = []; self.multiselectFilters[filter] = [];
}); });

View File

@ -47,6 +47,11 @@ TEMPLATES = [
}, },
] ]
# Email
# https://docs.djangoproject.com/en/1.10/topics/email/
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_TIMEOUT = 5 # Timeout in seconds for blocking operations like the connection attempt
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/ # https://docs.djangoproject.com/en/1.10/topics/i18n/

View File

@ -160,7 +160,7 @@
<div class="col-xs-11 main-header"> <div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right"> <span class="form-inline text-right pull-right">
<!-- reset Filters --> <!-- reset Filters -->
<span class="sort-spacer pointer" ng-click="filter.reset()" <span class="sort-spacer pointer" ng-click="filter.reset(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode" ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}"> ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>

View File

@ -1165,8 +1165,8 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.filter.operateMultiselectFilter('state', id, danger); $scope.filter.operateMultiselectFilter('state', id, danger);
updateStateFilter(); updateStateFilter();
}; };
$scope.resetFilters = function () { $scope.resetFilters = function (danger) {
$scope.filter.reset(); $scope.filter.reset(danger);
updateStateFilter(); updateStateFilter();
}; };
// Sorting // Sorting

View File

@ -396,7 +396,7 @@
<!-- show all selected multiselectoptions --> <!-- show all selected multiselectoptions -->
<div> <div>
<!-- clear all filters --> <!-- clear all filters -->
<span class="spacer-left-lg pointer" ng-click="resetFilters()" <span class="spacer-left-lg pointer" ng-click="resetFilters(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode" ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}"> ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>

View File

@ -1,3 +1,5 @@
from textwrap import dedent
from openslides.core.config import ConfigVariable from openslides.core.config import ConfigVariable
@ -90,3 +92,43 @@ def get_config_variables():
weight=570, weight=570,
group='Participants', group='Participants',
subgroup='PDF') subgroup='PDF')
# Email
yield ConfigVariable(
name='users_email_sender',
default_value='noreply@yourdomain.com',
input_type='string',
label='Email sender',
weight=600,
group='Participants',
subgroup='Email')
yield ConfigVariable(
name='users_email_subject',
default_value='Your login for {event_name}',
input_type='string',
label='Email subject',
help_text='You can use {event_name} as a placeholder.',
weight=605,
group='Participants',
subgroup='Email')
yield ConfigVariable(
name='users_email_body',
default_value=dedent('''\
Dear {name},
this is your OpenSlides login for the event "{event_name}":
{url}
username: {username}
password: {password}
This email was generated automatically.'''),
input_type='text',
label='Email body',
help_text='Use these placeholders: {name}, {event_name}, {url}, {username}, {password}. The url referrs to the system url.',
weight=610,
group='Participants',
subgroup='Email')

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2017-11-28 08:15
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0005_personalnote_rework'),
]
operations = [
migrations.AddField(
model_name='user',
name='email',
field=models.EmailField(blank=True, max_length=254),
),
migrations.AddField(
model_name='user',
name='last_email_send',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -1,3 +1,5 @@
import smtplib
from random import choice from random import choice
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
@ -9,10 +11,14 @@ from django.contrib.auth.models import (
Permission, Permission,
PermissionsMixin, PermissionsMixin,
) )
from django.core import mail
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Prefetch, Q from django.db.models import Prefetch, Q
from django.utils import timezone
from jsonfield import JSONField from jsonfield import JSONField
from ..core.config import config
from ..core.models import Projector from ..core.models import Projector
from ..utils.collection import CollectionElement from ..utils.collection import CollectionElement
from ..utils.models import RESTModelMixin from ..utils.models import RESTModelMixin
@ -136,6 +142,12 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
max_length=255, max_length=255,
blank=True) blank=True)
email = models.EmailField(blank=True)
last_email_send = models.DateTimeField(
blank=True,
null=True)
# TODO: Try to remove the default argument in the following fields. # TODO: Try to remove the default argument in the following fields.
structure_level = models.CharField( structure_level = models.CharField(
@ -228,6 +240,53 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
""" """
raise RuntimeError('Do not use user.has_perm() but use openslides.utils.auth.has_perm') raise RuntimeError('Do not use user.has_perm() but use openslides.utils.auth.has_perm')
def send_invitation_email(self, connection, skip_autoupdate=False):
"""
Sends an invitation email to the users. Returns True on success, False on failiure.
May raise an ValidationError, if something went wrong.
"""
if not self.email:
return False
# Custom dict class that for formatstrings with entries like {not_existent}
# no error is raised and this is replaced with ''.
class format_dict(dict):
def __missing__(self, key):
return ''
message_format = format_dict({
'name': str(self),
'event_name': config['general_event_name'],
'url': config['users_pdf_url'],
'username': self.username,
'password': self.default_password})
message = config['users_email_body'].format(**message_format)
subject_format = format_dict({'event_name': config['general_event_name']})
subject = config['users_email_subject'].format(**subject_format)
# Create an email and send it.
email = mail.EmailMessage(subject, message, config['users_email_sender'], [self.email])
try:
count = connection.send_messages([email])
except smtplib.SMTPDataError as e:
error = e.smtp_code
helptext = ''
if error == 554:
helptext = ' Is the email sender correct?'
connection.close()
raise ValidationError({'detail': 'Error {}. Cannot send email.{}'.format(error, helptext)})
except smtplib.SMTPRecipientsRefused:
pass # Run into returning false later
else:
if count == 1:
self.email_send = True
self.last_email_send = timezone.now()
self.save(skip_autoupdate=skip_autoupdate)
return True
return False
class GroupManager(_GroupManager): class GroupManager(_GroupManager):
""" """

View File

@ -29,6 +29,8 @@ USERCANSEESERIALIZER_FIELDS = (
USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + ( USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + (
'email',
'last_email_send',
'comment', 'comment',
'is_active', 'is_active',
) )
@ -51,6 +53,7 @@ class UserFullSerializer(ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = USERCANSEEEXTRASERIALIZER_FIELDS + ('default_password',) fields = USERCANSEEEXTRASERIALIZER_FIELDS + ('default_password',)
read_only_fields = ('last_email_send',)
def validate(self, data): def validate(self, data):
""" """

View File

@ -12,7 +12,7 @@ angular.module('OpenSlidesApp.users.csv', [])
function ($filter, Group, gettextCatalog, CsvDownload) { function ($filter, Group, gettextCatalog, CsvDownload) {
var makeHeaderline = function () { var makeHeaderline = function () {
var headerline = ['Title', 'Given name', 'Surname', 'Structure level', 'Participant number', 'Groups', var headerline = ['Title', 'Given name', 'Surname', 'Structure level', 'Participant number', 'Groups',
'Comment', 'Is active', 'Is present', 'Is a committee', 'Initial password']; 'Comment', 'Is active', 'Is present', 'Is a committee', 'Initial password', 'Email'];
return _.map(headerline, function (entry) { return _.map(headerline, function (entry) {
return gettextCatalog.getString(entry); return gettextCatalog.getString(entry);
}); });
@ -38,6 +38,7 @@ angular.module('OpenSlidesApp.users.csv', [])
row.push(user.is_present ? '1' : '0'); row.push(user.is_present ? '1' : '0');
row.push(user.is_committee ? '1' : '0'); row.push(user.is_committee ? '1' : '0');
row.push('"' + user.default_password + '"'); row.push('"' + user.default_password + '"');
row.push('"' + user.email + '"');
csvRows.push(row); csvRows.push(row);
}); });
CsvDownload(csvRows, 'users-export.csv'); CsvDownload(csvRows, 'users-export.csv');
@ -58,10 +59,10 @@ angular.module('OpenSlidesApp.users.csv', [])
var csvRows = [makeHeaderline(), var csvRows = [makeHeaderline(),
// example entries // example entries
['Dr.', 'Max', 'Mustermann', 'Berlin','1234567890', csvGroups, 'xyz', '1', '1', '', ''], ['Dr.', 'Max', 'Mustermann', 'Berlin','1234567890', csvGroups, 'xyz', '1', '1', '', 'initialPassword', ''],
['', 'John', 'Doe', 'Washington','75/99/8-2', csvGroup, 'abc', '1', '1', '', ''], ['', 'John', 'Doe', 'Washington','75/99/8-2', csvGroup, 'abc', '1', '1', '', '', 'john.doe@email.com'],
['', 'Fred', 'Bloggs', 'London', '', '', '', '', '', '', ''], ['', 'Fred', 'Bloggs', 'London', '', '', '', '', '', '', '', ''],
['', '', 'Executive Board', '', '', '', '', '', '', '1', ''], ['', '', 'Executive Board', '', '', '', '', '', '', '1', '', ''],
]; ];
CsvDownload(csvRows, 'users-example.csv'); CsvDownload(csvRows, 'users-example.csv');

View File

@ -306,6 +306,13 @@ angular.module('OpenSlidesApp.users.site', [
} }
] ]
}, },
{
key: 'email',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Email')
},
},
{ {
className: "row", className: "row",
fieldGroup: [ fieldGroup: [
@ -457,6 +464,13 @@ angular.module('OpenSlidesApp.users.site', [
required: true required: true
}, },
}, },
{
key: 'email',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Email')
},
},
{ {
key: 'about_me', key: 'about_me',
type: 'editor', type: 'editor',
@ -621,6 +635,8 @@ angular.module('OpenSlidesApp.users.site', [
display_name: gettext('Structure level')}, display_name: gettext('Structure level')},
{name: 'comment', {name: 'comment',
display_name: gettext('Comment')}, display_name: gettext('Comment')},
{name: 'last_email_send',
display_name: gettext('Last email send')},
]; ];
// pagination // pagination
@ -735,6 +751,33 @@ angular.module('OpenSlidesApp.users.site', [
User.save(user); User.save(user);
}); });
}; };
// Send invitation emails
$scope.sendInvitationEmails = function () {
var user_ids = _
.chain($scope.usersFiltered)
.filter(function (user) {
return user.selected;
})
.map(function (user) {
return user.id;
})
.value();
$http.post('/rest/users/user/mass_invite_email/', {
user_ids: user_ids,
}).then(function (success) {
$scope.alert = {
msg: gettextCatalog.getString('Send %num% emails sucessfully.').replace('%num%', success.data.count),
type: 'success',
show: true,
};
$scope.isSelectMode = false;
$scope.uncheckAll();
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
$scope.isSelectMode = false;
$scope.uncheckAll();
});
};
// Export as PDF // Export as PDF
$scope.pdfExportUserList = function () { $scope.pdfExportUserList = function () {
@ -1077,7 +1120,7 @@ angular.module('OpenSlidesApp.users.site', [
}; };
var FIELDS = ['title', 'first_name', 'last_name', 'structure_level', 'number', var FIELDS = ['title', 'first_name', 'last_name', 'structure_level', 'number',
'groups', 'comment', 'is_active', 'is_present', 'is_committee', 'default_password']; 'groups', 'comment', 'is_active', 'is_present', 'is_committee', 'default_password', 'email'];
$scope.users = []; $scope.users = [];
$scope.onCsvChange = function (csv) { $scope.onCsvChange = function (csv) {
$scope.csvImporting = false; $scope.csvImporting = false;
@ -1721,6 +1764,14 @@ angular.module('OpenSlidesApp.users.site', [
gettext('WEP'); gettext('WEP');
gettext('WPA/WPA2'); gettext('WPA/WPA2');
gettext('No encryption'); gettext('No encryption');
gettext('Email');
gettext('Email sender');
gettext('Email subject');
gettext('Your login for {event_name}');
gettext('You can use {event_name} as a placeholder.');
gettext('Email body');
gettext('Dear {name},\n\nthis is your OpenSlides login for the event "{event_name}":\n {url}\n username: {username}\n password: {password}\n\nThis email was generated automatically.');
gettext('Use these placeholders: {name}, {event_name}, {url}, {username}, {password}. The url referrs to the system url.');
} }
]); ]);

View File

@ -32,6 +32,10 @@
</div> </div>
<label translate>Participant number</label> <label translate>Participant number</label>
{{ user.number }} {{ user.number }}
<label translate>Email</label>
{{ user.email }}
<label ng-if="user.last_email_send" translate>Last email send</label>
{{ user.last_email_send | date: 'yyyy-MM-dd HH:mm:ss' }}
<label translate>About me</label> <label translate>About me</label>
<div ng-bind-html="user.about_me | trusted"></div> <div ng-bind-html="user.about_me | trusted"></div>
</fieldset> </fieldset>

View File

@ -58,6 +58,7 @@
<translate>Is present</translate>, <translate>Is present</translate>,
<translate>Is committee</translate>, <translate>Is committee</translate>,
<translate>Initial password</translate> <translate>Initial password</translate>
<translate>Email</translate>
</code> </code>
<li translate>At least given name or surname have to be filled in. All <li translate>At least given name or surname have to be filled in. All
other fields are optional and may be empty. other fields are optional and may be empty.
@ -83,7 +84,8 @@
<th translate>Is active <th translate>Is active
<th translate>Is present <th translate>Is present
<th translate>Is committee <th translate>Is committee
<th translate>Initial password</th> <th translate>Initial password
<th translate>Email
<th ng-if="duplicates > 0"> <th ng-if="duplicates > 0">
<i class="fa fa-exclamation-triangle text-danger"></i> <i class="fa fa-exclamation-triangle text-danger"></i>
<strong class="text-danger" ng-if="duplicates == 1">1 <translate>duplicate</translate></strong> <strong class="text-danger" ng-if="duplicates == 1">1 <translate>duplicate</translate></strong>
@ -158,6 +160,8 @@
ng-click="user.is_committee = !user.is_committee"></i> ng-click="user.is_committee = !user.is_committee"></i>
<td> <td>
{{ user.default_password }} {{ user.default_password }}
<td>
{{ user.email }}
<td ng-if="duplicates > 0"> <td ng-if="duplicates > 0">
<div ng-if="user.duplicate" uib-tooltip="{{ user.duplicate_info }}" uib-dropdown> <div ng-if="user.duplicate" uib-tooltip="{{ user.duplicate_info }}" uib-dropdown>
<button id="UserAction{{ $index }}" type="button" class="btn btn-default btn-sm" <button id="UserAction{{ $index }}" type="button" class="btn btn-default btn-sm"

View File

@ -25,6 +25,9 @@
</div> </div>
<div class="details"> <div class="details">
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" close="alert={}">
{{ alert.msg }}
</div>
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
<!-- select mode --> <!-- select mode -->
@ -80,6 +83,7 @@
<option value="is_active" translate>Set/Unset 'is active'</option> <option value="is_active" translate>Set/Unset 'is active'</option>
<option value="is_present" translate>Set/Unset 'is present'</option> <option value="is_present" translate>Set/Unset 'is present'</option>
<option value="is_committee" translate>Set/Unset 'is a committee'</option> <option value="is_committee" translate>Set/Unset 'is a committee'</option>
<option value="send_invite_email" translate>Send invitation emails</option>
</select> </select>
<!-- delete button --> <!-- delete button -->
<a ng-show="selectedAction == 'delete'" <a ng-show="selectedAction == 'delete'"
@ -129,6 +133,11 @@
<span ng-if="selectedAction == 'is_present'" translate>Is not present</span> <span ng-if="selectedAction == 'is_present'" translate>Is not present</span>
<span ng-if="selectedAction == 'is_committee'" translate>Is not a committee</span> <span ng-if="selectedAction == 'is_committee'" translate>Is not a committee</span>
</a> </a>
<!-- send_invite_email -->
<a ng-show="selectedAction == 'send_invite_email'" class="btn btn-default btn-sm"
ng-click="sendInvitationEmails()">
<translate>Send invitation emails</translate>
</a>
</div> </div>
</div> </div>
@ -172,7 +181,7 @@
<div class="col-xs-11 main-header"> <div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right"> <span class="form-inline text-right pull-right">
<!-- reset Filters --> <!-- reset Filters -->
<span class="sort-spacer pointer" ng-click="filter.reset()" <span class="sort-spacer pointer" ng-click="filter.reset(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode" ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}"> ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
@ -414,6 +423,13 @@
{{ user.comment | limitTo:25}}{{ user.comment.length > 25 ? '...' : '' }} {{ user.comment | limitTo:25}}{{ user.comment.length > 25 ? '...' : '' }}
</div> </div>
</div> </div>
<div os-perms="users.can_manage" ng-show="user.last_email_send">
<div uib-tooltip="{{ 'Last email send to the user' | translate }}" tooltip-placement="top-left">
<i class="fa fa-envelope"></i>
{{ user.last_email_send | date: 'yyyy-MM-dd HH:mm:ss' }}
</div>
</div>
</small> </small>
</div> </div>
<div style="width: 40%;" class="pull-right" os-perms="users.can_see_extra_data"> <div style="width: 40%;" class="pull-right" os-perms="users.can_see_extra_data">

View File

@ -1,10 +1,13 @@
import smtplib
from typing import List # noqa from typing import List # noqa
from django.conf import settings
from django.contrib.auth import login as auth_login from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout from django.contrib.auth import logout as auth_logout
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.core import mail
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import transaction from django.db import transaction
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -59,7 +62,7 @@ class UserViewSet(ModelViewSet):
result = has_perm(self.request.user, 'users.can_see_name') result = has_perm(self.request.user, 'users.can_see_name')
elif self.action in ('update', 'partial_update'): elif self.action in ('update', 'partial_update'):
result = self.request.user.is_authenticated() result = self.request.user.is_authenticated()
elif self.action in ('create', 'destroy', 'reset_password', 'mass_import'): elif self.action in ('create', 'destroy', 'reset_password', 'mass_import', 'mass_invite_email'):
result = (has_perm(self.request.user, 'users.can_see_name') and result = (has_perm(self.request.user, 'users.can_see_name') and
has_perm(self.request.user, 'users.can_see_extra_data') and has_perm(self.request.user, 'users.can_see_extra_data') and
has_perm(self.request.user, 'users.can_manage')) has_perm(self.request.user, 'users.can_manage'))
@ -165,6 +168,44 @@ class UserViewSet(ModelViewSet):
'detail': _('{number} users successfully imported.').format(number=len(created_users)), 'detail': _('{number} users successfully imported.').format(number=len(created_users)),
'importedTrackIds': imported_track_ids}) 'importedTrackIds': imported_track_ids})
@list_route(methods=['post'])
def mass_invite_email(self, request):
"""
Endpoint to send invitation emails to all given users (by id). Returns the
number of emails send.
"""
user_ids = request.data.get('user_ids')
if not isinstance(user_ids, list):
raise ValidationError({'detail': 'User_ids has to be a list.'})
for user_id in user_ids:
if not isinstance(user_id, int):
raise ValidationError({'detail': 'User_id has to be an int.'})
users = User.objects.filter(pk__in=user_ids)
# Sending Emails. Keep track, which users gets an email.
# First, try to open the connection to the smtp server.
connection = mail.get_connection(fail_silently=False)
try:
connection.open()
except ConnectionRefusedError:
raise ValidationError({'detail': 'Cannot connect to SMTP server on {}:{}'.format(
settings.EMAIL_HOST,
settings.EMAIL_PORT)})
except smtplib.SMTPException as e:
raise ValidationError({'detail': '{}: {}'.format(e.errno, e.strerror)})
success_users = []
try:
for user in users:
if user.send_invitation_email(connection, skip_autoupdate=True):
success_users.append(user)
except DjangoValidationError as e:
raise ValidationError(e.message_dict)
connection.close()
inform_changed_data(success_users)
return Response({'count': len(success_users)})
class GroupViewSetMetadata(SimpleMetadata): class GroupViewSetMetadata(SimpleMetadata):
""" """

View File

@ -43,6 +43,13 @@ SECRET_KEY = %(secret_key)r
DEBUG = %(debug)s DEBUG = %(debug)s
# Email settings
# For SSL/TLS specific settings see https://docs.djangoproject.com/en/1.11/topics/email/#smtp-backend
EMAIL_HOST = 'localhost'
EMAIL_PORT = 587
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
# Database # Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases # https://docs.djangoproject.com/en/1.10/ref/settings/#databases

View File

@ -1,3 +1,4 @@
from django.core import mail
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django_redis import get_redis_connection from django_redis import get_redis_connection
from rest_framework import status from rest_framework import status
@ -336,6 +337,30 @@ class UserMassImport(TestCase):
self.assertEqual(User.objects.count(), 3) self.assertEqual(User.objects.count(), 3)
class UserSendIntivationEmail(TestCase):
"""
Tests sending an email to the user.
"""
email = "admin@test-domain.com"
def setUp(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
self.admin = User.objects.get()
self.admin.email = self.email
self.admin.save()
def test_email_sending(self):
response = self.client.post(
reverse('user-mass-invite-email'),
{'user_ids': [self.admin.pk]},
format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to[0], self.email)
class GroupMetadata(TestCase): class GroupMetadata(TestCase):
def test_options_request_as_anonymous_user_activated(self): def test_options_request_as_anonymous_user_activated(self):
config['general_system_enable_anonymous'] = True config['general_system_enable_anonymous'] = True

View File

@ -10,6 +10,7 @@ from openslides.global_settings import * # noqa
OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__))) OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
# OpenSlides plugins # OpenSlides plugins