Send invitation emails with OpenSlides login.
This commit is contained in:
parent
a34ad1485a
commit
2220112d27
@ -71,6 +71,7 @@ Users:
|
||||
default is now disabled [#3400].
|
||||
- Hide password in change password view [#3417].
|
||||
- Added a change presence view [#3496].
|
||||
- New feature to send invitation emails with OpenSlides login [#3503].
|
||||
|
||||
Core:
|
||||
- No reload on logoff. OpenSlides is now a full single page
|
||||
|
@ -159,7 +159,7 @@
|
||||
<div class="col-xs-11 main-header">
|
||||
<span class="form-inline text-right pull-right">
|
||||
<!-- 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-class="{'disabled': isSelectMode}">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
|
@ -93,7 +93,7 @@
|
||||
<div class="col-xs-11 main-header">
|
||||
<span class="form-inline text-right pull-right">
|
||||
<!-- 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-class="{'disabled': isSelectMode}">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
|
@ -709,8 +709,9 @@ angular.module('OpenSlidesApp.core', [
|
||||
message += gettextCatalog.getString("The server didn't respond.");
|
||||
} else if (error.data.detail) {
|
||||
message += error.data.detail;
|
||||
} else if (error.status === 500) {
|
||||
message += gettextCatalog.getString("A server error occurred. Please check the system logs.");
|
||||
} else if (error.status > 500) { // Some kind of server error.
|
||||
message += gettextCatalog.getString("A server error occurred (%%code%%). Please check the system logs.");
|
||||
message = message.replace('%%code%%', error.status);
|
||||
} else {
|
||||
for (var e in error.data) {
|
||||
message += e + ': ' + error.data[e] + ' ';
|
||||
|
@ -535,7 +535,10 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
areFiltersSet = areFiltersSet || (self.filterString !== '');
|
||||
return areFiltersSet !== false;
|
||||
};
|
||||
self.reset = function () {
|
||||
self.reset = function (danger) {
|
||||
if (danger) {
|
||||
return;
|
||||
}
|
||||
_.forEach(self.multiselectFilters, function (filterList, filter) {
|
||||
self.multiselectFilters[filter] = [];
|
||||
});
|
||||
|
@ -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
|
||||
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
||||
|
@ -160,7 +160,7 @@
|
||||
<div class="col-xs-11 main-header">
|
||||
<span class="form-inline text-right pull-right">
|
||||
<!-- 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-class="{'disabled': isSelectMode}">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
|
@ -1165,8 +1165,8 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.filter.operateMultiselectFilter('state', id, danger);
|
||||
updateStateFilter();
|
||||
};
|
||||
$scope.resetFilters = function () {
|
||||
$scope.filter.reset();
|
||||
$scope.resetFilters = function (danger) {
|
||||
$scope.filter.reset(danger);
|
||||
updateStateFilter();
|
||||
};
|
||||
// Sorting
|
||||
|
@ -396,7 +396,7 @@
|
||||
<!-- show all selected multiselectoptions -->
|
||||
<div>
|
||||
<!-- 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-class="{'disabled': isSelectMode}">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
|
@ -1,3 +1,5 @@
|
||||
from textwrap import dedent
|
||||
|
||||
from openslides.core.config import ConfigVariable
|
||||
|
||||
|
||||
@ -90,3 +92,43 @@ def get_config_variables():
|
||||
weight=570,
|
||||
group='Participants',
|
||||
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')
|
||||
|
25
openslides/users/migrations/0006_user_email.py
Normal file
25
openslides/users/migrations/0006_user_email.py
Normal 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),
|
||||
),
|
||||
]
|
@ -1,3 +1,5 @@
|
||||
import smtplib
|
||||
|
||||
from random import choice
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
@ -9,10 +11,14 @@ from django.contrib.auth.models import (
|
||||
Permission,
|
||||
PermissionsMixin,
|
||||
)
|
||||
from django.core import mail
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.utils import timezone
|
||||
from jsonfield import JSONField
|
||||
|
||||
from ..core.config import config
|
||||
from ..core.models import Projector
|
||||
from ..utils.collection import CollectionElement
|
||||
from ..utils.models import RESTModelMixin
|
||||
@ -136,6 +142,12 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
||||
max_length=255,
|
||||
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.
|
||||
|
||||
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')
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -29,6 +29,8 @@ USERCANSEESERIALIZER_FIELDS = (
|
||||
|
||||
|
||||
USERCANSEEEXTRASERIALIZER_FIELDS = USERCANSEESERIALIZER_FIELDS + (
|
||||
'email',
|
||||
'last_email_send',
|
||||
'comment',
|
||||
'is_active',
|
||||
)
|
||||
@ -51,6 +53,7 @@ class UserFullSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = USERCANSEEEXTRASERIALIZER_FIELDS + ('default_password',)
|
||||
read_only_fields = ('last_email_send',)
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
|
@ -12,7 +12,7 @@ angular.module('OpenSlidesApp.users.csv', [])
|
||||
function ($filter, Group, gettextCatalog, CsvDownload) {
|
||||
var makeHeaderline = function () {
|
||||
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 gettextCatalog.getString(entry);
|
||||
});
|
||||
@ -38,6 +38,7 @@ angular.module('OpenSlidesApp.users.csv', [])
|
||||
row.push(user.is_present ? '1' : '0');
|
||||
row.push(user.is_committee ? '1' : '0');
|
||||
row.push('"' + user.default_password + '"');
|
||||
row.push('"' + user.email + '"');
|
||||
csvRows.push(row);
|
||||
});
|
||||
CsvDownload(csvRows, 'users-export.csv');
|
||||
@ -58,10 +59,10 @@ angular.module('OpenSlidesApp.users.csv', [])
|
||||
|
||||
var csvRows = [makeHeaderline(),
|
||||
// example entries
|
||||
['Dr.', 'Max', 'Mustermann', 'Berlin','1234567890', csvGroups, 'xyz', '1', '1', '', ''],
|
||||
['', 'John', 'Doe', 'Washington','75/99/8-2', csvGroup, 'abc', '1', '1', '', ''],
|
||||
['', 'Fred', 'Bloggs', 'London', '', '', '', '', '', '', ''],
|
||||
['', '', 'Executive Board', '', '', '', '', '', '', '1', ''],
|
||||
['Dr.', 'Max', 'Mustermann', 'Berlin','1234567890', csvGroups, 'xyz', '1', '1', '', 'initialPassword', ''],
|
||||
['', 'John', 'Doe', 'Washington','75/99/8-2', csvGroup, 'abc', '1', '1', '', '', 'john.doe@email.com'],
|
||||
['', 'Fred', 'Bloggs', 'London', '', '', '', '', '', '', '', ''],
|
||||
['', '', 'Executive Board', '', '', '', '', '', '', '1', '', ''],
|
||||
|
||||
];
|
||||
CsvDownload(csvRows, 'users-example.csv');
|
||||
|
@ -306,6 +306,13 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Email')
|
||||
},
|
||||
},
|
||||
{
|
||||
className: "row",
|
||||
fieldGroup: [
|
||||
@ -457,6 +464,13 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
required: true
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Email')
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'about_me',
|
||||
type: 'editor',
|
||||
@ -621,6 +635,8 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
display_name: gettext('Structure level')},
|
||||
{name: 'comment',
|
||||
display_name: gettext('Comment')},
|
||||
{name: 'last_email_send',
|
||||
display_name: gettext('Last email send')},
|
||||
];
|
||||
|
||||
// pagination
|
||||
@ -735,6 +751,33 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
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
|
||||
$scope.pdfExportUserList = function () {
|
||||
@ -1077,7 +1120,7 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
};
|
||||
|
||||
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.onCsvChange = function (csv) {
|
||||
$scope.csvImporting = false;
|
||||
@ -1721,6 +1764,14 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
gettext('WEP');
|
||||
gettext('WPA/WPA2');
|
||||
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.');
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -32,6 +32,10 @@
|
||||
</div>
|
||||
<label translate>Participant number</label>
|
||||
{{ 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>
|
||||
<div ng-bind-html="user.about_me | trusted"></div>
|
||||
</fieldset>
|
||||
|
@ -58,6 +58,7 @@
|
||||
<translate>Is present</translate>,
|
||||
<translate>Is committee</translate>,
|
||||
<translate>Initial password</translate>
|
||||
<translate>Email</translate>
|
||||
</code>
|
||||
<li translate>At least given name or surname have to be filled in. All
|
||||
other fields are optional and may be empty.
|
||||
@ -83,7 +84,8 @@
|
||||
<th translate>Is active
|
||||
<th translate>Is present
|
||||
<th translate>Is committee
|
||||
<th translate>Initial password</th>
|
||||
<th translate>Initial password
|
||||
<th translate>Email
|
||||
<th ng-if="duplicates > 0">
|
||||
<i class="fa fa-exclamation-triangle text-danger"></i>
|
||||
<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>
|
||||
<td>
|
||||
{{ user.default_password }}
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<td ng-if="duplicates > 0">
|
||||
<div ng-if="user.duplicate" uib-tooltip="{{ user.duplicate_info }}" uib-dropdown>
|
||||
<button id="UserAction{{ $index }}" type="button" class="btn btn-default btn-sm"
|
||||
|
@ -25,6 +25,9 @@
|
||||
</div>
|
||||
|
||||
<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="col-sm-6">
|
||||
<!-- select mode -->
|
||||
@ -80,6 +83,7 @@
|
||||
<option value="is_active" translate>Set/Unset 'is active'</option>
|
||||
<option value="is_present" translate>Set/Unset 'is present'</option>
|
||||
<option value="is_committee" translate>Set/Unset 'is a committee'</option>
|
||||
<option value="send_invite_email" translate>Send invitation emails</option>
|
||||
</select>
|
||||
<!-- delete button -->
|
||||
<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_committee'" translate>Is not a committee</span>
|
||||
</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>
|
||||
|
||||
@ -172,7 +181,7 @@
|
||||
<div class="col-xs-11 main-header">
|
||||
<span class="form-inline text-right pull-right">
|
||||
<!-- 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-class="{'disabled': isSelectMode}">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
@ -414,6 +423,13 @@
|
||||
{{ user.comment | limitTo:25}}{{ user.comment.length > 25 ? '...' : '' }}
|
||||
</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>
|
||||
</div>
|
||||
<div style="width: 40%;" class="pull-right" os-perms="users.can_see_extra_data">
|
||||
|
@ -1,10 +1,13 @@
|
||||
import smtplib
|
||||
from typing import List # noqa
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import login as auth_login
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core import mail
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.encoding import force_text
|
||||
@ -59,7 +62,7 @@ class UserViewSet(ModelViewSet):
|
||||
result = has_perm(self.request.user, 'users.can_see_name')
|
||||
elif self.action in ('update', 'partial_update'):
|
||||
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
|
||||
has_perm(self.request.user, 'users.can_see_extra_data') and
|
||||
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)),
|
||||
'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):
|
||||
"""
|
||||
|
@ -43,6 +43,13 @@ SECRET_KEY = %(secret_key)r
|
||||
|
||||
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
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
||||
|
@ -1,3 +1,4 @@
|
||||
from django.core import mail
|
||||
from django.core.urlresolvers import reverse
|
||||
from django_redis import get_redis_connection
|
||||
from rest_framework import status
|
||||
@ -336,6 +337,30 @@ class UserMassImport(TestCase):
|
||||
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):
|
||||
def test_options_request_as_anonymous_user_activated(self):
|
||||
config['general_system_enable_anonymous'] = True
|
||||
|
@ -10,6 +10,7 @@ from openslides.global_settings import * # noqa
|
||||
|
||||
OPENSLIDES_USER_DATA_PATH = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||
|
||||
# OpenSlides plugins
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user