Improve importing and sorting of users
* Add is_present field for csv import of users. * Refactor JS functions get_full_name and get_short_name (Fixes #2136). - Show participant number in get_full_name() output. - Sort users by first or last name. - Extend config option to sort users. - Mark unused Python methods get_short_name and get_full_name.
This commit is contained in:
parent
4c08bca34a
commit
5b544ceed2
@ -442,7 +442,10 @@ angular.module('OpenSlidesApp.agenda.site', [
|
||||
!item.speaker_list_closed &&
|
||||
$.inArray(operator.user.id, nextUsers) == -1);
|
||||
case 'remove':
|
||||
return ($.inArray(operator.user.id, nextUsers) != -1);
|
||||
if (operator.user) {
|
||||
return ($.inArray(operator.user.id, nextUsers) != -1);
|
||||
}
|
||||
return false;
|
||||
case 'removeAll':
|
||||
return (operator.hasPerms('agenda.can_manage') &&
|
||||
$scope.speakers.length > 0);
|
||||
|
@ -10,14 +10,16 @@ def get_config_variables():
|
||||
"""
|
||||
# Sorting
|
||||
yield ConfigVariable(
|
||||
name='users_sort_users_by_first_name',
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Sort users by first name',
|
||||
help_text='Disable for sorting by last name',
|
||||
name='users_sort_by',
|
||||
default_value='firstName',
|
||||
input_type='choice',
|
||||
label='Sort name of participants by',
|
||||
choices=(
|
||||
{'value': 'firstName', 'display_name': 'First name'},
|
||||
{'value': 'lastName', 'display_name': 'Last name'}),
|
||||
weight=510,
|
||||
group='Participants',
|
||||
subgroup='Sorting')
|
||||
subgroup='General')
|
||||
|
||||
# PDF
|
||||
|
||||
|
@ -13,7 +13,6 @@ from django.db.models import Q
|
||||
|
||||
from openslides.utils.search import user_name_helper
|
||||
|
||||
from ..core.config import config
|
||||
from ..utils.models import RESTModelMixin
|
||||
from .access_permissions import UserAccessPermissions
|
||||
|
||||
@ -175,38 +174,13 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
||||
ordering = ('last_name', 'first_name', 'username', )
|
||||
|
||||
def __str__(self):
|
||||
return self.get_full_name()
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Returns a long form of the name.
|
||||
|
||||
E. g.: * Dr. Max Mustermann (Villingen)
|
||||
* Professor Dr. Enders, Christoph (Leipzig)
|
||||
"""
|
||||
structure = '(%s)' % self.structure_level if self.structure_level else ''
|
||||
return ' '.join((self.title, self.get_short_name(), structure)).strip()
|
||||
|
||||
def get_short_name(self, sort_by_first_name=None):
|
||||
"""
|
||||
Returns only the name of the user.
|
||||
|
||||
E. g.: * Max Mustermann
|
||||
* Enders, Christoph
|
||||
"""
|
||||
# Strip white spaces from the name parts
|
||||
first_name = self.first_name.strip()
|
||||
last_name = self.last_name.strip()
|
||||
|
||||
# The user has a last_name and a first_name
|
||||
if first_name and last_name:
|
||||
if sort_by_first_name is None:
|
||||
sort_by_first_name = config['users_sort_users_by_first_name']
|
||||
if sort_by_first_name:
|
||||
name = ' '.join((first_name, last_name))
|
||||
else:
|
||||
name = ', '.join((last_name, first_name))
|
||||
|
||||
name = ' '.join((self.first_name, self.last_name))
|
||||
# The user has only a first_name or a last_name or no name
|
||||
else:
|
||||
name = first_name or last_name or self.username
|
||||
@ -214,6 +188,14 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
||||
# Return result
|
||||
return name
|
||||
|
||||
# TODO: remove this function after PR#2476 is merged. (see Issue#2594)
|
||||
def get_full_name(self):
|
||||
return ''
|
||||
|
||||
# TODO: remove this function after PR#2476 is merged. (see Issue#2594)
|
||||
def get_short_name(self):
|
||||
return ''
|
||||
|
||||
def get_search_index_string(self):
|
||||
"""
|
||||
Returns a string that can be indexed for the search.
|
||||
|
@ -24,10 +24,7 @@ def users_to_pdf(pdf):
|
||||
Create a list of all users as PDF.
|
||||
"""
|
||||
data = [['#', _('Name'), _('Structure level'), _('Group')]]
|
||||
if config['users_sort_users_by_first_name']:
|
||||
sort = 'first_name'
|
||||
else:
|
||||
sort = 'last_name'
|
||||
sort = 'first_name'
|
||||
counter = 0
|
||||
for user in User.objects.all().order_by(sort):
|
||||
counter += 1
|
||||
@ -73,10 +70,7 @@ def users_passwords_to_pdf(pdf):
|
||||
users_pdf_welcometitle = config["users_pdf_welcometitle"]
|
||||
users_pdf_welcometext = config["users_pdf_welcometext"]
|
||||
|
||||
if config['users_sort_users_by_first_name']:
|
||||
sort = 'first_name'
|
||||
else:
|
||||
sort = 'last_name'
|
||||
sort = 'first_name'
|
||||
qrcode_size = 2 * cm
|
||||
# qrcode for system url
|
||||
qrcode_url = QrCodeWidget(users_pdf_url)
|
||||
|
@ -69,7 +69,8 @@ angular.module('OpenSlidesApp.users', [])
|
||||
'Group',
|
||||
'jsDataModel',
|
||||
'gettext',
|
||||
function(DS, Group, jsDataModel, gettext) {
|
||||
'Config',
|
||||
function(DS, Group, jsDataModel, gettext, Config) {
|
||||
var name = 'users/user';
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
@ -88,41 +89,54 @@ angular.module('OpenSlidesApp.users', [])
|
||||
getResourceName: function () {
|
||||
return name;
|
||||
},
|
||||
/*
|
||||
* Returns a short form of the name.
|
||||
*
|
||||
* Example:
|
||||
* - Dr. Max Mustermann
|
||||
* - Professor Dr. Enders, Christoph
|
||||
*/
|
||||
get_short_name: function() {
|
||||
// should be the same as in the python user model.
|
||||
var title = _.trim(this.title),
|
||||
firstName = _.trim(this.first_name),
|
||||
lastName = _.trim(this.last_name),
|
||||
name = '';
|
||||
|
||||
if (title) {
|
||||
name = title + ' ';
|
||||
}
|
||||
if (firstName && lastName) {
|
||||
name += [firstName, lastName].join(' ');
|
||||
if (Config.get('users_sort_by').value == 'lastName') {
|
||||
if (lastName && firstName) {
|
||||
name += [lastName, firstName].join(', ');
|
||||
} else {
|
||||
name += lastName || firstName;
|
||||
}
|
||||
} else {
|
||||
name += firstName || lastName || this.username;
|
||||
name += [firstName, lastName].join(' ');
|
||||
}
|
||||
if (title !== '') {
|
||||
name = title + ' ' + name;
|
||||
}
|
||||
return name;
|
||||
},
|
||||
/*
|
||||
* Returns a long form of the name.
|
||||
*
|
||||
* Example:
|
||||
* - Dr. Max Mustermann (Villingen)
|
||||
* - Professor Dr. Enders, Christoph (Leipzig)
|
||||
*/
|
||||
get_full_name: function() {
|
||||
// should be the same as in the python user model.
|
||||
var title = _.trim(this.title),
|
||||
firstName = _.trim(this.first_name),
|
||||
lastName = _.trim(this.last_name),
|
||||
var name = this.get_short_name(),
|
||||
structure_level = _.trim(this.structure_level),
|
||||
name = '';
|
||||
number = _.trim(this.number),
|
||||
addition = [];
|
||||
|
||||
if (title) {
|
||||
name = title + ' ';
|
||||
}
|
||||
if (firstName && lastName) {
|
||||
name += [firstName, lastName].join(' ');
|
||||
} else {
|
||||
name += firstName || lastName || this.username;
|
||||
// addition: add number and structure level
|
||||
if (number) {
|
||||
addition.push(number);
|
||||
}
|
||||
if (structure_level) {
|
||||
name += " (" + structure_level + ")";
|
||||
addition.push(structure_level);
|
||||
}
|
||||
if (addition.length > 0) {
|
||||
name += ' (' + addition.join(', ') + ')';
|
||||
}
|
||||
return name;
|
||||
},
|
||||
|
@ -897,6 +897,17 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
} else {
|
||||
user.is_active = false;
|
||||
}
|
||||
// is present
|
||||
if (user.is_present) {
|
||||
user.is_present = user.is_present.replace(quotionRe, '$1');
|
||||
if (user.is_present == '1') {
|
||||
user.is_present = true;
|
||||
} else {
|
||||
user.is_present = false;
|
||||
}
|
||||
} else {
|
||||
user.is_present = false;
|
||||
}
|
||||
// is committee
|
||||
if (user.is_committee) {
|
||||
user.is_committee = user.is_committee.replace(quotionRe, '$1');
|
||||
@ -1016,12 +1027,12 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
var element = document.getElementById('downloadLink');
|
||||
var csvRows = [
|
||||
// column header line
|
||||
['title', 'first_name', 'last_name', 'structure_level', 'number', 'groups', 'comment', 'is_active', 'is_committee'],
|
||||
['title', 'first_name', 'last_name', 'structure_level', 'number', 'groups', 'comment', 'is_active', 'is_present', 'is_committee'],
|
||||
// example entries
|
||||
['Dr.', 'Max', 'Mustermann', 'Berlin','1234567890', '"3,4"', 'xyz', '1', ''],
|
||||
['', 'John', 'Doe', 'Washington','75/99/8-2', '3', 'abc', '1', ''],
|
||||
['', 'Fred', 'Bloggs', 'London', '', '', '', '', ''],
|
||||
['', '', 'Executive Board', '', '', '5', '', '', '1'],
|
||||
['Dr.', 'Max', 'Mustermann', 'Berlin','1234567890', '"3,4"', 'xyz', '1', '1', ''],
|
||||
['', 'John', 'Doe', 'Washington','75/99/8-2', '3', 'abc', '1', '1', ''],
|
||||
['', 'Fred', 'Bloggs', 'London', '', '', '', '', '', ''],
|
||||
['', '', 'Executive Board', '', '', '5', '', '', '', '1'],
|
||||
|
||||
];
|
||||
var csvString = csvRows.join("%0A");
|
||||
@ -1373,14 +1384,15 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
gettext('Can manage users');
|
||||
|
||||
// config strings in users/config_variables.py
|
||||
gettext('[Place for your welcome and help text.]');
|
||||
gettext('Sort users by first name');
|
||||
gettext('Disable for sorting by last name');
|
||||
gettext('General');
|
||||
gettext('Sort name of participants by');
|
||||
gettext('Participants');
|
||||
gettext('Sorting');
|
||||
gettext('First name');
|
||||
gettext('Last name');
|
||||
gettext('PDF');
|
||||
gettext('Welcome to OpenSlides');
|
||||
gettext('Title for access data and welcome PDF');
|
||||
gettext('PDF');
|
||||
gettext('[Place for your welcome and help text.]');
|
||||
gettext('Help text for access data and welcome PDF');
|
||||
gettext('System URL');
|
||||
gettext('Used for QRCode in PDF of access data.');
|
||||
|
@ -68,7 +68,7 @@
|
||||
<h4 translate>Please note:</h4>
|
||||
<ul class="indentation">
|
||||
<li><translate>Required comma or semicolon separated values with these column header names in the first row</translate>:<br>
|
||||
<code>title, first_name, last_name, structure_level, number, groups, comment, is_active, is_committee</code>
|
||||
<code>title, first_name, last_name, structure_level, number, groups, comment, is_active, is_present, is_committee</code>
|
||||
<li><translate>Default groups</translate>:
|
||||
<translate>Delegates</translate> <code>2</code>,
|
||||
<translate>Staff</translate> <code>3</code>
|
||||
@ -94,6 +94,7 @@
|
||||
<th translate>Groups
|
||||
<th translate>Comment
|
||||
<th translate>Is active
|
||||
<th translate>Is present
|
||||
<th translate>Is committee</th>
|
||||
<th ng-if="duplicates > 0">
|
||||
<i class="fa fa-exclamation-triangle text-danger"></i>
|
||||
@ -155,6 +156,9 @@
|
||||
<td>
|
||||
<i class="fa pointer" ng-class="user.is_active ? 'fa-check-square-o' : 'fa-square-o'"
|
||||
ng-click="user.is_active = !user.is_active"></i>
|
||||
<td>
|
||||
<i class="fa pointer" ng-class="user.is_present ? 'fa-check-square-o' : 'fa-square-o'"
|
||||
ng-click="user.is_present = !user.is_present"></i>
|
||||
<td>
|
||||
<i class="fa pointer" ng-class="user.is_committee ? 'fa-check-square-o' : 'fa-square-o'"
|
||||
ng-click="user.is_committee = !user.is_committee"></i>
|
||||
|
@ -97,10 +97,9 @@ def user_name_helper(users):
|
||||
then the str(users) is returned.
|
||||
"""
|
||||
if isinstance(users, list) or isinstance(users, QuerySet):
|
||||
user_string = " ".join(
|
||||
user.get_short_name(sort_by_first_name=True) for user in users)
|
||||
user_string = " ".join(str(user) for user in users)
|
||||
elif isinstance(users, get_user_model()):
|
||||
user_string = users.get_short_name(sort_by_first_name=True)
|
||||
user_string = str(users)
|
||||
else:
|
||||
user_string = str(users)
|
||||
return user_string
|
||||
|
@ -1,175 +1,7 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from openslides.users.models import User, UserManager
|
||||
|
||||
|
||||
class UserTest(TestCase):
|
||||
def test_str(self):
|
||||
"""
|
||||
Tests, that the str representaion of a User object returns the same as
|
||||
User.get_full_name().
|
||||
"""
|
||||
user = User()
|
||||
user.get_full_name = MagicMock(return_value='Test Value IJee1yoet1ooGhesh5li')
|
||||
|
||||
self.assertEqual(
|
||||
str(user),
|
||||
'Test Value IJee1yoet1ooGhesh5li',
|
||||
"The str representation of User is not user.get_full_name().")
|
||||
|
||||
|
||||
class UserGetFullName(TestCase):
|
||||
def test_get_full_name_with_structure_level_and_title(self):
|
||||
"""
|
||||
Tests that get_full_name returns the write string.
|
||||
"""
|
||||
user = User()
|
||||
user.title = 'test_title'
|
||||
user.structure_level = 'test_structure_level'
|
||||
user.get_short_name = MagicMock(return_value='test_short_name')
|
||||
|
||||
self.assertEqual(
|
||||
user.get_full_name(),
|
||||
'test_title test_short_name (test_structure_level)',
|
||||
"User.get_full_name() returns wrong string when it has a "
|
||||
"structure_level and title.")
|
||||
|
||||
def test_get_full_name_without_structure_level_and_with_title(self):
|
||||
"""
|
||||
Tests that get_full_name returns the write string.
|
||||
"""
|
||||
user = User()
|
||||
user.title = 'test_title'
|
||||
user.structure_level = ''
|
||||
user.get_short_name = MagicMock(return_value='test_short_name')
|
||||
|
||||
self.assertEqual(
|
||||
user.get_full_name(),
|
||||
'test_title test_short_name',
|
||||
"User.get_full_name() returns wrong string when it has no "
|
||||
"structure_level but a title.")
|
||||
|
||||
def test_get_full_name_without_structure_level_and_without_title(self):
|
||||
"""
|
||||
Tests that get_full_name returns the write string.
|
||||
"""
|
||||
user = User()
|
||||
user.title = ''
|
||||
user.structure_level = ''
|
||||
user.get_short_name = MagicMock(return_value='test_short_name')
|
||||
|
||||
self.assertEqual(
|
||||
user.get_full_name(),
|
||||
'test_short_name',
|
||||
"User.get_full_name() returns wrong string when it has no "
|
||||
"structure_level and no title.")
|
||||
|
||||
|
||||
class UserGetShortName(TestCase):
|
||||
def test_get_short_name_sort_first_name_only_first_name(self):
|
||||
"""
|
||||
Tests the output of get_short_name.
|
||||
"""
|
||||
user = User()
|
||||
user.first_name = 'test_first_name'
|
||||
|
||||
with patch('openslides.users.models.config') as mock_config:
|
||||
mock_config.__getitem__.return_value = True
|
||||
short_name = user.get_short_name()
|
||||
|
||||
self.assertEqual(
|
||||
short_name,
|
||||
'test_first_name',
|
||||
"User.get_short_name() returns wrong string when it has only a "
|
||||
"first_name and is sorted by first_name.")
|
||||
|
||||
def test_get_short_name_sort_first_name_both_names(self):
|
||||
"""
|
||||
Tests the output of get_short_name.
|
||||
"""
|
||||
user = User()
|
||||
user.first_name = 'test_first_name'
|
||||
user.last_name = 'test_last_name'
|
||||
|
||||
with patch('openslides.users.models.config') as mock_config:
|
||||
mock_config.__getitem__.return_value = True
|
||||
short_name = user.get_short_name()
|
||||
|
||||
self.assertEqual(
|
||||
short_name,
|
||||
'test_first_name test_last_name',
|
||||
"User.get_short_name() returns wrong string when it has a fist_name "
|
||||
"and a last_name and is sorted by first_name.")
|
||||
|
||||
def test_get_short_name_sort_last_name_only_first_name(self):
|
||||
"""
|
||||
Tests the output of get_short_name.
|
||||
"""
|
||||
user = User()
|
||||
user.first_name = 'test_first_name'
|
||||
|
||||
with patch('openslides.users.models.config') as mock_config:
|
||||
mock_config.__getitem__.return_value = False
|
||||
short_name = user.get_short_name()
|
||||
|
||||
self.assertEqual(
|
||||
short_name,
|
||||
'test_first_name',
|
||||
"User.get_short_name() returns wrong string when it has only a "
|
||||
"first_name and is sorted by last_name.")
|
||||
|
||||
def test_get_short_name_sort_last_name_both_names(self):
|
||||
"""
|
||||
Tests the output of get_short_name.
|
||||
"""
|
||||
user = User()
|
||||
user.first_name = 'test_first_name'
|
||||
user.last_name = 'test_last_name'
|
||||
|
||||
with patch('openslides.users.models.config') as mock_config:
|
||||
mock_config.__getitem__.return_value = False
|
||||
short_name = user.get_short_name()
|
||||
|
||||
self.assertEqual(
|
||||
short_name,
|
||||
'test_last_name, test_first_name',
|
||||
"User.get_short_name() returns wrong string when it has a fist_name "
|
||||
"and a last_name and is sorted by last_name.")
|
||||
|
||||
def test_get_short_name_no_names(self):
|
||||
"""
|
||||
Tests the output of get_short_name.
|
||||
"""
|
||||
user = User(username='test_username')
|
||||
|
||||
with patch('openslides.users.models.config') as mock_config:
|
||||
mock_config.__getitem__.return_value = False
|
||||
short_name = user.get_short_name()
|
||||
|
||||
self.assertEqual(
|
||||
short_name,
|
||||
'test_username',
|
||||
"User.get_short_name() returns wrong string when it has no fist_name "
|
||||
"and no last_name and is sorted by last_name.")
|
||||
|
||||
def test_while_spaces_in_name_parts(self):
|
||||
"""
|
||||
Tests the output if the name parts have white spaces at the begin or
|
||||
end.
|
||||
"""
|
||||
user = User()
|
||||
user.first_name = ' test_first_name\n '
|
||||
user.last_name = ' test_last_name \n'
|
||||
|
||||
with patch('openslides.users.models.config') as mock_config:
|
||||
mock_config.__getitem__.return_value = True
|
||||
short_name = user.get_short_name()
|
||||
|
||||
self.assertEqual(
|
||||
short_name,
|
||||
'test_first_name test_last_name',
|
||||
"User.get_short_name() has to strip whitespaces from the name parts.")
|
||||
from openslides.users.models import UserManager
|
||||
|
||||
|
||||
class UserManagerTest(TestCase):
|
||||
|
Loading…
Reference in New Issue
Block a user