Merge pull request #3503 from FinnStutzenstein/emailTest
Send invitation emails
This commit is contained in:
commit
2fe1218fbb
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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] + ' ';
|
||||||
|
@ -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] = [];
|
||||||
});
|
});
|
||||||
|
@ -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/
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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')
|
||||||
|
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 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):
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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');
|
||||||
|
@ -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.');
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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">
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user