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:
Emanuel Schütze 2016-11-04 13:26:44 +01:00
parent 4c08bca34a
commit 5b544ceed2
9 changed files with 89 additions and 247 deletions

View File

@ -442,7 +442,10 @@ angular.module('OpenSlidesApp.agenda.site', [
!item.speaker_list_closed && !item.speaker_list_closed &&
$.inArray(operator.user.id, nextUsers) == -1); $.inArray(operator.user.id, nextUsers) == -1);
case 'remove': case 'remove':
return ($.inArray(operator.user.id, nextUsers) != -1); if (operator.user) {
return ($.inArray(operator.user.id, nextUsers) != -1);
}
return false;
case 'removeAll': case 'removeAll':
return (operator.hasPerms('agenda.can_manage') && return (operator.hasPerms('agenda.can_manage') &&
$scope.speakers.length > 0); $scope.speakers.length > 0);

View File

@ -10,14 +10,16 @@ def get_config_variables():
""" """
# Sorting # Sorting
yield ConfigVariable( yield ConfigVariable(
name='users_sort_users_by_first_name', name='users_sort_by',
default_value=False, default_value='firstName',
input_type='boolean', input_type='choice',
label='Sort users by first name', label='Sort name of participants by',
help_text='Disable for sorting by last name', choices=(
{'value': 'firstName', 'display_name': 'First name'},
{'value': 'lastName', 'display_name': 'Last name'}),
weight=510, weight=510,
group='Participants', group='Participants',
subgroup='Sorting') subgroup='General')
# PDF # PDF

View File

@ -13,7 +13,6 @@ from django.db.models import Q
from openslides.utils.search import user_name_helper from openslides.utils.search import user_name_helper
from ..core.config import config
from ..utils.models import RESTModelMixin from ..utils.models import RESTModelMixin
from .access_permissions import UserAccessPermissions from .access_permissions import UserAccessPermissions
@ -175,38 +174,13 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
ordering = ('last_name', 'first_name', 'username', ) ordering = ('last_name', 'first_name', 'username', )
def __str__(self): 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 # Strip white spaces from the name parts
first_name = self.first_name.strip() first_name = self.first_name.strip()
last_name = self.last_name.strip() last_name = self.last_name.strip()
# The user has a last_name and a first_name # The user has a last_name and a first_name
if first_name and last_name: if first_name and last_name:
if sort_by_first_name is None: name = ' '.join((self.first_name, self.last_name))
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))
# The user has only a first_name or a last_name or no name # The user has only a first_name or a last_name or no name
else: else:
name = first_name or last_name or self.username name = first_name or last_name or self.username
@ -214,6 +188,14 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
# Return result # Return result
return name 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): def get_search_index_string(self):
""" """
Returns a string that can be indexed for the search. Returns a string that can be indexed for the search.

View File

@ -24,10 +24,7 @@ def users_to_pdf(pdf):
Create a list of all users as PDF. Create a list of all users as PDF.
""" """
data = [['#', _('Name'), _('Structure level'), _('Group')]] data = [['#', _('Name'), _('Structure level'), _('Group')]]
if config['users_sort_users_by_first_name']: sort = 'first_name'
sort = 'first_name'
else:
sort = 'last_name'
counter = 0 counter = 0
for user in User.objects.all().order_by(sort): for user in User.objects.all().order_by(sort):
counter += 1 counter += 1
@ -73,10 +70,7 @@ def users_passwords_to_pdf(pdf):
users_pdf_welcometitle = config["users_pdf_welcometitle"] users_pdf_welcometitle = config["users_pdf_welcometitle"]
users_pdf_welcometext = config["users_pdf_welcometext"] users_pdf_welcometext = config["users_pdf_welcometext"]
if config['users_sort_users_by_first_name']: sort = 'first_name'
sort = 'first_name'
else:
sort = 'last_name'
qrcode_size = 2 * cm qrcode_size = 2 * cm
# qrcode for system url # qrcode for system url
qrcode_url = QrCodeWidget(users_pdf_url) qrcode_url = QrCodeWidget(users_pdf_url)

View File

@ -69,7 +69,8 @@ angular.module('OpenSlidesApp.users', [])
'Group', 'Group',
'jsDataModel', 'jsDataModel',
'gettext', 'gettext',
function(DS, Group, jsDataModel, gettext) { 'Config',
function(DS, Group, jsDataModel, gettext, Config) {
var name = 'users/user'; var name = 'users/user';
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
@ -88,41 +89,54 @@ angular.module('OpenSlidesApp.users', [])
getResourceName: function () { getResourceName: function () {
return name; return name;
}, },
/*
* Returns a short form of the name.
*
* Example:
* - Dr. Max Mustermann
* - Professor Dr. Enders, Christoph
*/
get_short_name: function() { get_short_name: function() {
// should be the same as in the python user model.
var title = _.trim(this.title), var title = _.trim(this.title),
firstName = _.trim(this.first_name), firstName = _.trim(this.first_name),
lastName = _.trim(this.last_name), lastName = _.trim(this.last_name),
name = ''; name = '';
if (Config.get('users_sort_by').value == 'lastName') {
if (title) { if (lastName && firstName) {
name = title + ' '; name += [lastName, firstName].join(', ');
} } else {
if (firstName && lastName) { name += lastName || firstName;
name += [firstName, lastName].join(' '); }
} else { } else {
name += firstName || lastName || this.username; name += [firstName, lastName].join(' ');
}
if (title !== '') {
name = title + ' ' + name;
} }
return name; return name;
}, },
/*
* Returns a long form of the name.
*
* Example:
* - Dr. Max Mustermann (Villingen)
* - Professor Dr. Enders, Christoph (Leipzig)
*/
get_full_name: function() { get_full_name: function() {
// should be the same as in the python user model. var name = this.get_short_name(),
var title = _.trim(this.title),
firstName = _.trim(this.first_name),
lastName = _.trim(this.last_name),
structure_level = _.trim(this.structure_level), structure_level = _.trim(this.structure_level),
name = ''; number = _.trim(this.number),
addition = [];
if (title) { // addition: add number and structure level
name = title + ' '; if (number) {
} addition.push(number);
if (firstName && lastName) {
name += [firstName, lastName].join(' ');
} else {
name += firstName || lastName || this.username;
} }
if (structure_level) { if (structure_level) {
name += " (" + structure_level + ")"; addition.push(structure_level);
}
if (addition.length > 0) {
name += ' (' + addition.join(', ') + ')';
} }
return name; return name;
}, },

View File

@ -897,6 +897,17 @@ angular.module('OpenSlidesApp.users.site', [
} else { } else {
user.is_active = false; 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 // is committee
if (user.is_committee) { if (user.is_committee) {
user.is_committee = user.is_committee.replace(quotionRe, '$1'); user.is_committee = user.is_committee.replace(quotionRe, '$1');
@ -1016,12 +1027,12 @@ angular.module('OpenSlidesApp.users.site', [
var element = document.getElementById('downloadLink'); var element = document.getElementById('downloadLink');
var csvRows = [ var csvRows = [
// column header line // 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 // example entries
['Dr.', 'Max', 'Mustermann', 'Berlin','1234567890', '"3,4"', 'xyz', '1', ''], ['Dr.', 'Max', 'Mustermann', 'Berlin','1234567890', '"3,4"', 'xyz', '1', '1', ''],
['', 'John', 'Doe', 'Washington','75/99/8-2', '3', 'abc', '1', ''], ['', 'John', 'Doe', 'Washington','75/99/8-2', '3', 'abc', '1', '1', ''],
['', 'Fred', 'Bloggs', 'London', '', '', '', '', ''], ['', 'Fred', 'Bloggs', 'London', '', '', '', '', '', ''],
['', '', 'Executive Board', '', '', '5', '', '', '1'], ['', '', 'Executive Board', '', '', '5', '', '', '', '1'],
]; ];
var csvString = csvRows.join("%0A"); var csvString = csvRows.join("%0A");
@ -1373,14 +1384,15 @@ angular.module('OpenSlidesApp.users.site', [
gettext('Can manage users'); gettext('Can manage users');
// config strings in users/config_variables.py // config strings in users/config_variables.py
gettext('[Place for your welcome and help text.]'); gettext('General');
gettext('Sort users by first name'); gettext('Sort name of participants by');
gettext('Disable for sorting by last name');
gettext('Participants'); gettext('Participants');
gettext('Sorting'); gettext('First name');
gettext('Last name');
gettext('PDF');
gettext('Welcome to OpenSlides'); gettext('Welcome to OpenSlides');
gettext('Title for access data and welcome PDF'); 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('Help text for access data and welcome PDF');
gettext('System URL'); gettext('System URL');
gettext('Used for QRCode in PDF of access data.'); gettext('Used for QRCode in PDF of access data.');

View File

@ -68,7 +68,7 @@
<h4 translate>Please note:</h4> <h4 translate>Please note:</h4>
<ul class="indentation"> <ul class="indentation">
<li><translate>Required comma or semicolon separated values with these column header names in the first row</translate>:<br> <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>: <li><translate>Default groups</translate>:
<translate>Delegates</translate> <code>2</code>, <translate>Delegates</translate> <code>2</code>,
<translate>Staff</translate> <code>3</code> <translate>Staff</translate> <code>3</code>
@ -94,6 +94,7 @@
<th translate>Groups <th translate>Groups
<th translate>Comment <th translate>Comment
<th translate>Is active <th translate>Is active
<th translate>Is present
<th translate>Is committee</th> <th translate>Is committee</th>
<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>
@ -155,6 +156,9 @@
<td> <td>
<i class="fa pointer" ng-class="user.is_active ? 'fa-check-square-o' : 'fa-square-o'" <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> 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> <td>
<i class="fa pointer" ng-class="user.is_committee ? 'fa-check-square-o' : 'fa-square-o'" <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> ng-click="user.is_committee = !user.is_committee"></i>

View File

@ -97,10 +97,9 @@ def user_name_helper(users):
then the str(users) is returned. then the str(users) is returned.
""" """
if isinstance(users, list) or isinstance(users, QuerySet): if isinstance(users, list) or isinstance(users, QuerySet):
user_string = " ".join( user_string = " ".join(str(user) for user in users)
user.get_short_name(sort_by_first_name=True) for user in users)
elif isinstance(users, get_user_model()): elif isinstance(users, get_user_model()):
user_string = users.get_short_name(sort_by_first_name=True) user_string = str(users)
else: else:
user_string = str(users) user_string = str(users)
return user_string return user_string

View File

@ -1,175 +1,7 @@
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, call, patch from unittest.mock import MagicMock, call, patch
from openslides.users.models import User, UserManager from openslides.users.models import 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.")
class UserManagerTest(TestCase): class UserManagerTest(TestCase):