Merge pull request #3647 from FinnStutzenstein/submitterSort2

Sort submitters
This commit is contained in:
Emanuel Schütze 2018-06-13 14:34:31 +02:00 committed by GitHub
commit 5735cebcf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 702 additions and 143 deletions

View File

@ -27,7 +27,7 @@ script:
- node_modules/.bin/karma start --browsers PhantomJS tests/karma/karma.conf.js - node_modules/.bin/karma start --browsers PhantomJS tests/karma/karma.conf.js
- DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.unit - DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.unit
- coverage report --fail-under=43 - coverage report --fail-under=42
- DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.integration - DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.integration
- coverage report --fail-under=73 - coverage report --fail-under=73

View File

@ -8,7 +8,8 @@ Version 2.3 (unreleased)
======================== ========================
Motions: Motions:
- New feature to scroll the projector to a specific line [#3748]. - New feature to scroll the projector to a specific line [#3748].
- New possibility to sort submitters [#3647].
Version 2.2 (2018-06-06) Version 2.2 (2018-06-06)

View File

@ -1,15 +1,15 @@
<div ng-if="item" class="details" ng-controller="ListOfSpeakersManagementCtrl"> <div ng-if="item" class="details" ng-controller="ListOfSpeakersManagementCtrl">
<div class="speakers-toolbar"> <div class="speakers-toolbar">
<div class="pull-right"> <div class="pull-right">
<span os-perms="agenda.can_manage"> <span os-perms="agenda.can_manage">
<button ng-if="item.speaker_list_closed" ng-click="closeList(false)" <button ng-if="item.speaker_list_closed" ng-click="closeList(false)"
class="btn btn-sm btn-default"> class="btn btn-sm btn-default">
<translate>Open list of speakers</translate> <translate>Open list of speakers</translate>
</button> </button>
<button ng-if="!item.speaker_list_closed" ng-click="closeList(true)" <button ng-if="!item.speaker_list_closed" ng-click="closeList(true)"
class="btn btn-sm btn-default"> class="btn btn-sm btn-default">
<translate>Close list of speakers</translate> <translate>Close list of speakers</translate>
</button> </button>
</span> </span>
<span os-perms="agenda.can_manage_list_of_speakers"> <span os-perms="agenda.can_manage_list_of_speakers">
<button ng-if="isAllowed('removeAll')" class="btn btn-sm btn-danger" <button ng-if="isAllowed('removeAll')" class="btn btn-sm btn-danger"
@ -20,7 +20,7 @@
</button> </button>
</span> </span>
</div> </div>
</div> </div>
<!-- text for empty list --> <!-- text for empty list -->
<p ng-if="speakers.length == 0" translate> <p ng-if="speakers.length == 0" translate>
@ -79,7 +79,7 @@
<div ng-show="nextSpeakers.length > 0"> <div ng-show="nextSpeakers.length > 0">
<div ui-tree="treeOptions" data-empty-placeholder-enabled="false"> <div ui-tree="treeOptions" data-empty-placeholder-enabled="false">
<ol ui-tree-nodes="" ng-model="nextSpeakers"> <ol ui-tree-nodes="" ng-model="nextSpeakers">
<li ng-repeat="speaker in nextSpeakers | orderBy:'weight'" ui-tree-node> <li ng-repeat="speaker in nextSpeakers | orderBy: 'weight'" ui-tree-node>
<i os-perms="agenda.can_manage_list_of_speakers" ui-tree-handle="" class="fa fa-arrows-v"></i> <i os-perms="agenda.can_manage_list_of_speakers" ui-tree-handle="" class="fa fa-arrows-v"></i>
{{ $index + 1 }}. {{ $index + 1 }}.
{{ speaker.user.get_full_name() }} {{ speaker.user.get_full_name() }}

View File

@ -274,11 +274,8 @@ strong, b, th {
.meta { .meta {
h3 {
font-family: $font-condensed-light;
}
.heading, h3 { .heading, h3 {
font-family: $font-condensed-light;
font-size: 22px; font-size: 22px;
line-height: 24px; line-height: 24px;
font-weight: 300; font-weight: 300;

View File

@ -43,7 +43,8 @@ class MotionAccessPermissions(BaseAccessPermissions):
for full in full_data: for full in full_data:
# Check if user is submitter of this motion. # Check if user is submitter of this motion.
if isinstance(user, CollectionElement): if isinstance(user, CollectionElement):
is_submitter = user.get_full_data()['id'] in full.get('submitters_id', []) is_submitter = user.get_full_data()['id'] in [
submitter['user_id'] for submitter in full.get('submitters', [])]
else: else:
# Anonymous users can not be submitters. # Anonymous users can not be submitters.
is_submitter = False is_submitter = False

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-02-09 07:18
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.db import migrations, models
import openslides.utils.models
def move_submitters_to_own_model(apps, schema_editor):
Motion = apps.get_model('motions', 'Motion')
Submitter = apps.get_model('motions', 'Submitter')
for motion in Motion.objects.all():
weight = 0
for user in motion.submittersOld.all():
# We cannot use the add method here, so do it manually...
if Submitter.objects.filter(user=user, motion=motion).exists():
continue # The user is already a submitter. Skip this duplicate.
if isinstance(user, AnonymousUser):
continue # Skip the anonymous
submitter = Submitter(user=user, motion=motion, weight=weight)
submitter.save(force_insert=True)
weight += 1
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('motions', '0005_auto_20180202_1318'),
]
operations = [
migrations.RenameField(
model_name='motion',
old_name='submitters',
new_name='submittersOld',
),
migrations.CreateModel(
name='Submitter',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('weight', models.IntegerField(null=True)),
],
options={
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model),
),
migrations.AddField(
model_name='submitter',
name='motion',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submitters', to='motions.Motion'),
),
migrations.AddField(
model_name='submitter',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.RunPython(
move_submitters_to_own_model
),
migrations.RemoveField(
model_name='motion',
name='submittersOld',
),
]

View File

@ -1,6 +1,7 @@
from typing import Any, Dict # noqa from typing import Any, Dict # noqa
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db import IntegrityError, models, transaction from django.db import IntegrityError, models, transaction
@ -21,6 +22,7 @@ from openslides.poll.models import (
CollectDefaultVotesMixin, CollectDefaultVotesMixin,
) )
from openslides.utils.autoupdate import inform_changed_data from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin from openslides.utils.models import RESTModelMixin
from .access_permissions import ( from .access_permissions import (
@ -157,11 +159,6 @@ class Motion(RESTModelMixin, models.Model):
Tags to categorise motions. Tags to categorise motions.
""" """
submitters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_submitters', blank=True)
"""
Users who submit this motion.
"""
supporters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_supporters', blank=True) supporters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_supporters', blank=True)
""" """
Users who support this motion. Users who support this motion.
@ -548,7 +545,7 @@ class Motion(RESTModelMixin, models.Model):
""" """
Returns True if user is a submitter of this motion, else False. Returns True if user is a submitter of this motion, else False.
""" """
return user in self.submitters.all() return self.submitters.filter(user=user).exists()
def is_supporter(self, user): def is_supporter(self, user):
""" """
@ -706,6 +703,69 @@ class Motion(RESTModelMixin, models.Model):
yield from amendment.get_amendments_deep() yield from amendment.get_amendments_deep()
class SubmitterManager(models.Manager):
"""
Manager for Submitter model. Provides a customized add method.
"""
def add(self, user, motion, skip_autoupdate=False):
"""
Customized manager method to prevent anonymous users to be a
submitter and that someone is not twice a submitter. Cares also
for the initial sorting of the submitters.
"""
if self.filter(user=user, motion=motion).exists():
raise OpenSlidesError(
_('{user} is already a submitter.').format(user=user))
if isinstance(user, AnonymousUser):
raise OpenSlidesError(
_('An anonymous user can not be a submitter.'))
weight = (self.filter(motion=motion).aggregate(
models.Max('weight'))['weight__max'] or 0)
submitter = self.model(user=user, motion=motion, weight=weight + 1)
submitter.save(force_insert=True, skip_autoupdate=skip_autoupdate)
return submitter
class Submitter(RESTModelMixin, models.Model):
"""
M2M Model for submitters.
"""
objects = SubmitterManager()
"""
Use custom Manager.
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
"""
ForeignKey to the user who is the submitter.
"""
motion = models.ForeignKey(
Motion,
on_delete=models.CASCADE,
related_name='submitters')
"""
ForeignKey to the motion.
"""
weight = models.IntegerField(null=True)
class Meta:
default_permissions = ()
def __str__(self):
return str(self.user)
def get_root_rest_element(self):
"""
Returns the motion to this instance which is the root REST element.
"""
return self.motion
class MotionVersion(RESTModelMixin, models.Model): class MotionVersion(RESTModelMixin, models.Model):
""" """
A MotionVersion object saves some date of the motion. A MotionVersion object saves some date of the motion.

View File

@ -29,7 +29,8 @@ class MotionSlide(ProjectorElement):
yield motion.agenda_item yield motion.agenda_item
yield motion.state.workflow yield motion.state.workflow
yield from self.required_motions_for_state_and_recommendation(motion) yield from self.required_motions_for_state_and_recommendation(motion)
yield from motion.submitters.all() for submitter in motion.submitters.all():
yield submitter.user
yield from motion.supporters.all() yield from motion.supporters.all()
yield from MotionChangeRecommendation.objects.filter(motion_version=motion.get_active_version().id) yield from MotionChangeRecommendation.objects.filter(motion_version=motion.get_active_version().id)

View File

@ -24,6 +24,7 @@ from .models import (
MotionPoll, MotionPoll,
MotionVersion, MotionVersion,
State, State,
Submitter,
Workflow, Workflow,
) )
@ -290,6 +291,20 @@ class MotionChangeRecommendationSerializer(ModelSerializer):
return data return data
class SubmitterSerializer(ModelSerializer):
"""
Serializer for motion.models.Submitter objects.
"""
class Meta:
model = Submitter
fields = (
'id',
'user',
'motion',
'weight',
)
class MotionSerializer(ModelSerializer): class MotionSerializer(ModelSerializer):
""" """
Serializer for motion.models.Motion objects. Serializer for motion.models.Motion objects.
@ -310,6 +325,7 @@ class MotionSerializer(ModelSerializer):
write_only=True) write_only=True)
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2) agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
submitters = SubmitterSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Motion model = Motion
@ -374,10 +390,6 @@ class MotionSerializer(ModelSerializer):
motion.agenda_item_update_information['type'] = validated_data.get('agenda_type') motion.agenda_item_update_information['type'] = validated_data.get('agenda_type')
motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id') motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id')
motion.save() motion.save()
if validated_data.get('submitters'):
motion.submitters.add(*validated_data['submitters'])
elif validated_data['request_user'].is_authenticated():
motion.submitters.add(validated_data['request_user'])
motion.supporters.add(*validated_data.get('supporters', [])) motion.supporters.add(*validated_data.get('supporters', []))
motion.attachments.add(*validated_data.get('attachments', [])) motion.attachments.add(*validated_data.get('attachments', []))
motion.tags.add(*validated_data.get('tags', [])) motion.tags.add(*validated_data.get('tags', []))

View File

@ -130,6 +130,7 @@ def required_users(sender, request_user, **kwargs):
if has_perm(request_user, 'motions.can_see'): if has_perm(request_user, 'motions.can_see'):
for motion_collection_element in Collection(Motion.get_collection_string()).element_generator(): for motion_collection_element in Collection(Motion.get_collection_string()).element_generator():
full_data = motion_collection_element.get_full_data() full_data = motion_collection_element.get_full_data()
submitters_supporters.update(full_data['submitters_id']) submitters_supporters.update(
[submitter['user_id'] for submitter in full_data['submitters']])
submitters_supporters.update(full_data['supporters_id']) submitters_supporters.update(full_data['supporters_id'])
return submitters_supporters return submitters_supporters

View File

@ -209,6 +209,22 @@ angular.module('OpenSlidesApp.motions', [
} }
]) ])
.factory('Submitter', [
'DS',
function (DS) {
return DS.defineResource({
name: 'motions/submitter',
relations: {
belongsTo: {
'users/user': {
localField: 'user',
localKey: 'user_id',
}
}
}
});
}
])
.factory('Motion', [ .factory('Motion', [
'DS', 'DS',
@ -571,6 +587,7 @@ angular.module('OpenSlidesApp.motions', [
* There are the following possible actions. * There are the following possible actions.
* - see * - see
* - update * - update
* - update_submitters
* - delete * - delete
* - create_poll * - create_poll
* - support * - support
@ -604,6 +621,8 @@ angular.module('OpenSlidesApp.motions', [
this.state.allow_submitter_edit this.state.allow_submitter_edit
) )
); );
case 'update_submitters':
return operator.hasPerms('motions.can_manage');
case 'delete': case 'delete':
return ( return (
operator.hasPerms('motions.can_manage') || operator.hasPerms('motions.can_manage') ||
@ -755,20 +774,18 @@ angular.module('OpenSlidesApp.motions', [
localField: 'attachments', localField: 'attachments',
localKeys: 'attachments_id', localKeys: 'attachments_id',
}, },
'users/user': [ 'users/user': {
{
localField: 'submitters',
localKeys: 'submitters_id',
},
{
localField: 'supporters', localField: 'supporters',
localKeys: 'supporters_id', localKeys: 'supporters_id',
} },
],
'motions/motion-poll': { 'motions/motion-poll': {
localField: 'polls', localField: 'polls',
foreignKey: 'motion_id', foreignKey: 'motion_id',
} },
'motions/submitter': {
localField: 'submitters',
foreignKey: 'motion_id',
},
}, },
hasOne: { hasOne: {
'motions/workflowstate': [ 'motions/workflowstate': [
@ -987,7 +1004,8 @@ angular.module('OpenSlidesApp.motions', [
'Category', 'Category',
'Workflow', 'Workflow',
'MotionChangeRecommendation', 'MotionChangeRecommendation',
function(Motion, Category, Workflow, MotionChangeRecommendation) {} 'Submitter',
function(Motion, Category, Workflow, MotionChangeRecommendation, Submitter) {}
]) ])

View File

@ -5,11 +5,12 @@
angular.module('OpenSlidesApp.motions.csv', []) angular.module('OpenSlidesApp.motions.csv', [])
.factory('MotionCsvExport', [ .factory('MotionCsvExport', [
'$filter',
'gettextCatalog', 'gettextCatalog',
'Config', 'Config',
'CsvDownload', 'CsvDownload',
'lineNumberingService', 'lineNumberingService',
function (gettextCatalog, Config, CsvDownload, lineNumberingService) { function ($filter, gettextCatalog, Config, CsvDownload, lineNumberingService) {
var makeHeaderline = function (params) { var makeHeaderline = function (params) {
var headerline = ['Identifier', 'Title']; var headerline = ['Identifier', 'Title'];
if (params.include.text) { if (params.include.text) {
@ -77,8 +78,12 @@ angular.module('OpenSlidesApp.motions.csv', [])
// Submitters // Submitters
if (params.include.submitters) { if (params.include.submitters) {
var submitters = []; var submitters = [];
angular.forEach(motion.submitters, function(user) { _.forEach($filter('orderBy')(motion.submitters, 'weight'), function (user) {
var user_short_name = [user.title, user.first_name, user.last_name].join(' ').trim(); var user_short_name = [
user.user.title,
user.user.first_name,
user.user.last_name
].join(' ').trim();
submitters.push(user_short_name); submitters.push(user_short_name);
}); });
row.push('"' + submitters.join('; ') + '"'); row.push('"' + submitters.join('; ') + '"');

View File

@ -7,6 +7,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
.factory('MotionDocxExport', [ .factory('MotionDocxExport', [
'$http', '$http',
'$q', '$q',
'$filter',
'operator', 'operator',
'Config', 'Config',
'Category', 'Category',
@ -15,8 +16,8 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
'lineNumberingService', 'lineNumberingService',
'Html2DocxConverter', 'Html2DocxConverter',
'MotionComment', 'MotionComment',
function ($http, $q, operator, Config, Category, gettextCatalog, FileSaver, lineNumberingService, function ($http, $q, $filter, operator, Config, Category, gettextCatalog,
Html2DocxConverter, MotionComment) { FileSaver, lineNumberingService, Html2DocxConverter, MotionComment) {
var PAGEBREAK = '<w:p><w:r><w:br w:type="page" /></w:r></w:p>'; var PAGEBREAK = '<w:p><w:r><w:br w:type="page" /></w:r></w:p>';
@ -116,9 +117,11 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
id: motion.id, id: motion.id,
identifier: motion.identifier || '', identifier: motion.identifier || '',
title: title, title: title,
submitters: params.include.submitters ? _.map(motion.submitters, function (submitter) { submitters: params.include.submitters ? _.map(
return submitter.get_full_name(); $filter('orderBy')(motion.submitters, 'weight'), function (submitter) {
}).join(', ') : '', return submitter.user.get_full_name();
}
).join(', ') : '',
status: motion.getStateName(), status: motion.getStateName(),
// Miscellaneous stuff // Miscellaneous stuff
preamble: gettextCatalog.getString(Config.get('motions_preamble').value), preamble: gettextCatalog.getString(Config.get('motions_preamble').value),

View File

@ -6,6 +6,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
.factory('MotionContentProvider', [ .factory('MotionContentProvider', [
'$q', '$q',
'$filter',
'operator', 'operator',
'gettextCatalog', 'gettextCatalog',
'PDFLayout', 'PDFLayout',
@ -17,7 +18,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'Motion', 'Motion',
'MotionComment', 'MotionComment',
'OpenSlidesSettings', 'OpenSlidesSettings',
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, function($q, $filter, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter,
HTMLValidizer, Category, Config, Motion, MotionComment, OpenSlidesSettings) { HTMLValidizer, Category, Config, Motion, MotionComment, OpenSlidesSettings) {
/** /**
* Provides the content as JS objects for Motions in pdfMake context * Provides the content as JS objects for Motions in pdfMake context
@ -86,9 +87,11 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
var metaTableBody = []; var metaTableBody = [];
// submitters // submitters
var submitters = _.map(motion.submitters, function (submitter) { var submitters = _.map(
return submitter.get_full_name(); $filter('orderBy')(motion.submitters, 'weight'), function (submitter) {
}).join(', '); return submitter.user.get_full_name();
}
).join(', ');
if (params.include.submitters) { if (params.include.submitters) {
metaTableBody.push([ metaTableBody.push([
{ {

View File

@ -89,6 +89,19 @@ angular.module('OpenSlidesApp.motions.site', [
} }
] ]
}) })
.state('motions.motion.submitters', {
url: '/submitters/{id:int}',
controller: 'MotionSubmitterCtrl',
resolve: {
motionId: ['$stateParams', function($stateParams) {
return $stateParams.id;
}],
},
data: {
title: gettext('Submitters'),
basePerm: 'motions.can_manage',
},
})
.state('motions.motion.import', { .state('motions.motion.import', {
url: '/import', url: '/import',
controller: 'MotionImportCtrl', controller: 'MotionImportCtrl',
@ -388,26 +401,31 @@ angular.module('OpenSlidesApp.motions.site', [
getFormFields: function (isCreateForm) { getFormFields: function (isCreateForm) {
var workflows = Workflow.getAll(); var workflows = Workflow.getAll();
var images = Mediafile.getAllImages(); var images = Mediafile.getAllImages();
var formFields = [ var formFields = [];
{ formFields.push({
key: 'identifier', key: 'identifier',
type: 'input', type: 'input',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Identifier') label: gettextCatalog.getString('Identifier')
}, },
hide: true hide: true
}, });
{
if (isCreateForm) {
formFields.push({
key: 'submitters_id', key: 'submitters_id',
type: 'select-multiple', type: 'select-multiple',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Submitters'), label: gettextCatalog.getString('Submitters'),
options: User.getAll(), options: User.getAll(),
ngOptions: 'option.id as option.full_name for option in to.options', ngOptions: 'option.id as option.full_name for option in to.options',
placeholder: gettextCatalog.getString('Select or search a submitter ...') placeholder: gettextCatalog.getString('Select or search a submitter ...'),
}, },
hide: !operator.hasPerms('motions.can_manage') hide: !operator.hasPerms('motions.can_manage')
}, });
}
formFields = formFields.concat([
{ {
key: 'title', key: 'title',
type: 'input', type: 'input',
@ -448,7 +466,8 @@ angular.module('OpenSlidesApp.motions.site', [
description: gettextCatalog.getString("Don't create a new version.") description: gettextCatalog.getString("Don't create a new version.")
}, },
hide: true hide: true
}]; }
]);
// show as agenda item + parent item // show as agenda item + parent item
if (isCreateForm) { if (isCreateForm) {
@ -1192,7 +1211,7 @@ angular.module('OpenSlidesApp.motions.site', [
]; ];
$scope.filter.propertyDict = { $scope.filter.propertyDict = {
'submitters': function (submitter) { 'submitters': function (submitter) {
return submitter.get_short_name(); return submitter.user.get_short_name();
}, },
'supporters': function (supporter) { 'supporters': function (supporter) {
return supporter.get_short_name(); return supporter.get_short_name();
@ -1235,7 +1254,7 @@ angular.module('OpenSlidesApp.motions.site', [
display_name: gettext('Identifier')}, display_name: gettext('Identifier')},
{name: 'getTitle()', {name: 'getTitle()',
display_name: gettext('Title')}, display_name: gettext('Title')},
{name: 'submitters[0].get_short_name()', {name: 'submitters[0].user.get_short_name()',
display_name: gettext('Submitters')}, display_name: gettext('Submitters')},
{name: 'category.' + Config.get('motions_export_category_sorting').value, {name: 'category.' + Config.get('motions_export_category_sorting').value,
display_name: gettext('Category')}, display_name: gettext('Category')},
@ -2117,7 +2136,7 @@ angular.module('OpenSlidesApp.motions.site', [
'operator', 'operator',
'ErrorMessage', 'ErrorMessage',
'EditingWarning', 'EditingWarning',
function($scope, $state, Motion, Category, Config, Mediafile, MotionForm, function ($scope, $state, Motion, Category, Config, Mediafile, MotionForm,
Tag, User, Workflow, Agenda, motionId, operator, ErrorMessage, Tag, User, Workflow, Agenda, motionId, operator, ErrorMessage,
EditingWarning) { EditingWarning) {
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
@ -2176,7 +2195,7 @@ angular.module('OpenSlidesApp.motions.site', [
Motion.inject(motion); Motion.inject(motion);
// save changed motion object on server // save changed motion object on server
Motion.save(motion).then( Motion.save(motion).then(
function(success) { function (success) {
if (gotoDetailView) { if (gotoDetailView) {
$state.go('motions.motion.detail', {id: success.id}); $state.go('motions.motion.detail', {id: success.id});
} }
@ -2201,7 +2220,7 @@ angular.module('OpenSlidesApp.motions.site', [
'motionpollId', 'motionpollId',
'voteNumber', 'voteNumber',
'ErrorMessage', 'ErrorMessage',
function($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId, function ($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId,
voteNumber, ErrorMessage) { voteNumber, ErrorMessage) {
// set initial values for form model by create deep copy of motionpoll object // set initial values for form model by create deep copy of motionpoll object
// so detail view is not updated while editing poll // so detail view is not updated while editing poll
@ -2220,16 +2239,76 @@ angular.module('OpenSlidesApp.motions.site', [
votesinvalid: poll.votesinvalid, votesinvalid: poll.votesinvalid,
votescast: poll.votescast votescast: poll.votescast
}) })
.then(function(success) { .then(function (success) {
$scope.alert.show = false; $scope.alert.show = false;
$scope.closeThisDialog(); $scope.closeThisDialog();
}, function(error) { }, function (error) {
$scope.alert = ErrorMessage.forAlert(error); $scope.alert = ErrorMessage.forAlert(error);
}); });
}; };
} }
]) ])
.controller('MotionSubmitterCtrl', [
'$scope',
'$filter',
'$http',
'User',
'Motion',
'motionId',
'ErrorMessage',
function ($scope, $filter, $http, User, Motion, motionId, ErrorMessage) {
User.bindAll({}, $scope, 'users');
$scope.submitterSelectBox = {};
$scope.alert = {};
$scope.$watch(function () {
return Motion.lastModified(motionId);
}, function () {
$scope.motion = Motion.get(motionId);
$scope.submitters = $filter('orderBy')($scope.motion.submitters, 'weight');
});
$scope.addSubmitter = function (userId) {
$scope.submitterSelectBox = {};
$http.post('/rest/motions/motion/' + $scope.motion.id + '/manage_submitters/', {
'user': userId
}).then(
function (success) {
$scope.alert.show = false;
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
$scope.removeSubmitter = function (userId) {
$http.delete('/rest/motions/motion/' + $scope.motion.id + '/manage_submitters/', {
headers: {'Content-Type': 'application/json'},
data: JSON.stringify({user: userId})
}).then(
function (success) {
$scope.alert.show = false;
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
// save reordered list of submitters
$scope.treeOptions = {
dropped: function (event) {
var submitterIds = _.map($scope.submitters, function (submitter) {
return submitter.id;
});
$http.post('/rest/motions/motion/' + $scope.motion.id + '/sort_submitters/', {
submitters: submitterIds,
});
}
};
}
])
.controller('MotionImportCtrl', [ .controller('MotionImportCtrl', [
'$scope', '$scope',
'$q', '$q',
@ -2239,7 +2318,7 @@ angular.module('OpenSlidesApp.motions.site', [
'MotionBlock', 'MotionBlock',
'User', 'User',
'MotionCsvExport', 'MotionCsvExport',
function($scope, $q, gettext, Category, Motion, MotionBlock, User, MotionCsvExport) { function ($scope, $q, gettext, Category, Motion, MotionBlock, User, MotionCsvExport) {
// set initial data for csv import // set initial data for csv import
$scope.motions = []; $scope.motions = [];
@ -2295,7 +2374,7 @@ angular.module('OpenSlidesApp.motions.site', [
} }
// submitter // submitter
if (motion.submitter && motion.submitter !== '') { if (motion.submitter && motion.submitter !== '') {
angular.forEach(User.getAll(), function (user) { _.forEach(User.getAll(), function (user) {
var user_short_name = [user.title, user.first_name, user.last_name].join(' ').trim(); var user_short_name = [user.title, user.first_name, user.last_name].join(' ').trim();
if (user_short_name == motion.submitter.trim()) { if (user_short_name == motion.submitter.trim()) {
motion.submitters_id = [user.id]; motion.submitters_id = [user.id];

View File

@ -125,9 +125,15 @@
<div class="row"> <div class="row">
<div class="col-sm-4"> <div class="col-sm-4">
<!-- submitters --> <!-- submitters -->
<h3 translate>Submitters</h3> <h3 ng-if="!motion.isAllowed('update_submitters')" class="heading" translate>Submitters</h3>
<div ng-repeat="submitter in motion.submitters"> <h3 ng-if="motion.isAllowed('update_submitters')" class="heading">
{{ submitter.get_full_name() }} <translate>Submitters</translate>
<a ui-sref="motions.motion.submitters({id: motion.id})" uib-tooltip="{{ 'Sort submitters' | translate }}">
<i class="fa fa-cog"></i>
</a>
</h3>
<div ng-repeat="submitter in motion.submitters | orderBy: 'weight'">
{{ submitter.user.get_full_name() }}
</div> </div>
<!-- supporters --> <!-- supporters -->
@ -211,7 +217,7 @@
<div ng-if="motion.isAllowed('change_recommendation')" class="heading"> <div ng-if="motion.isAllowed('change_recommendation')" class="heading">
<span uib-dropdown> <span uib-dropdown>
<span id="recommendation-dropdown" class="drop-down-name pointer" uib-dropdown-toggle> <span id="recommendation-dropdown" class="drop-down-name pointer" uib-dropdown-toggle>
{{ config('motions_recommendations_by') }} {{ config('motions_recommendations_by') | translate }}
<i class="fa fa-cog"></i> <i class="fa fa-cog"></i>
</span> </span>
<ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="recommendation-dropdown"> <ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="recommendation-dropdown">

View File

@ -628,8 +628,8 @@
<div ng-if="motion.submitters.length"> <div ng-if="motion.submitters.length">
<small> <small>
<span class="optional" translate>by</span> <span class="optional" translate>by</span>
<span class="optional" ng-repeat="submitter in motion.submitters | limitTo:1"> <span class="optional" ng-repeat="submitter in motion.submitters | orderBy: 'weight' | limitTo:1">
{{ submitter.get_full_name() }}<span ng-if="!$last">,</span></span><span ng-if="motion.submitters.length > 1">, {{ submitter.user.get_full_name() }}<span ng-if="!$last">,</span></span><span ng-if="motion.submitters.length > 1">,
... [+{{ motion.submitters.length - 1 }}]</span> ... [+{{ motion.submitters.length - 1 }}]</span>
<!-- sorry for merging them together, but otherwise there would be a whitespace because of the new line --> <!-- sorry for merging them together, but otherwise there would be a whitespace because of the new line -->
</small> </small>
@ -640,6 +640,11 @@
<span ng-if="motion.isAllowed('update')"> <span ng-if="motion.isAllowed('update')">
<a href="" ng-click="openDialog(motion)" translate>Edit</a> <a href="" ng-click="openDialog(motion)" translate>Edit</a>
</span> </span>
<span ng-if="motion.isAllowed('update_submitters')"> &middot;
<a ui-sref="motions.motion.submitters({id: motion.id})" translate>
Edit submitters
</a>
</span>
<span ng-if="motion.isAllowed('delete')"> &middot; <span ng-if="motion.isAllowed('delete')"> &middot;
<a href="" class="text-danger" <a href="" class="text-danger"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br><b>{{ motion.getTitle() }}</b>" ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br><b>{{ motion.getTitle() }}</b>"

View File

@ -0,0 +1,44 @@
<div class="header">
<div class="title">
<div class="submenu">
<a href="javascript:history.back()" class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back</translate>
</a>
</div>
<h1>{{ motion.getAgendaTitle() }}</h1>
<h2 translate>Sort submitters</h2>
</div>
</div>
<div class="details">
<!-- Submitters -->
<div ui-tree="treeOptions" data-empty-placeholder-enabled="false">
<ol ui-tree-nodes="" ng-model="submitters">
<li ng-repeat="submitter in submitters" ui-tree-node>
<i ui-tree-handle="" class="fa fa-arrows-v"></i>
{{ $index + 1 }}.
{{ submitter.user.get_full_name() }}
<button ng-click="removeSubmitter(submitter.user.id)"
class="btn btn-default btn-sm" title="{{ 'Remove' | translate }}">
<i class="fa fa-times"></i>
</button>
</ol>
</div>
<!-- Select new submitter form -->
<div class="form-group spacer-top-lg">
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
{{ alert.msg }}
</div>
<select chosen
ng-model="submitterSelectBox.selected"
ng-change="addSubmitter(submitterSelectBox.selected)"
ng-options="user.id as user.get_full_name() for user in users"
search-contains="true"
placeholder-text-single="'Select or search a participant ...' | translate"
no-results-text="'No results available ...' | translate"
class="form-control">
<select>
</div>
</div>

View File

@ -12,8 +12,8 @@
<!-- Submitters --> <!-- Submitters -->
<h3 ng-if="motion.submitters.length > 0" translate>Submitters</h3> <h3 ng-if="motion.submitters.length > 0" translate>Submitters</h3>
<div ng-repeat="submitter in motion.submitters"> <div ng-repeat="submitter in motion.submitters | orderBy: 'weight'">
{{ submitter.get_full_name() }}<br> {{ submitter.user.get_full_name() }}<span ng-hide="$last">,</span>
</div> </div>
<!-- Poll results --> <!-- Poll results -->

View File

@ -2,9 +2,11 @@ import re
from typing import Optional # noqa from typing import Optional # noqa
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.http import Http404 from django.http import Http404
from django.http.request import QueryDict
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from rest_framework import status from rest_framework import status
@ -13,6 +15,7 @@ from ..core.config import config
from ..utils.auth import has_perm from ..utils.auth import has_perm
from ..utils.autoupdate import inform_changed_data from ..utils.autoupdate import inform_changed_data
from ..utils.collection import CollectionElement from ..utils.collection import CollectionElement
from ..utils.exceptions import OpenSlidesError
from ..utils.rest_api import ( from ..utils.rest_api import (
DestroyModelMixin, DestroyModelMixin,
GenericViewSet, GenericViewSet,
@ -39,6 +42,7 @@ from .models import (
MotionPoll, MotionPoll,
MotionVersion, MotionVersion,
State, State,
Submitter,
Workflow, Workflow,
) )
from .serializers import MotionPollSerializer from .serializers import MotionPollSerializer
@ -73,7 +77,8 @@ class MotionViewSet(ModelViewSet):
(not config['motions_stop_submitting'] or (not config['motions_stop_submitting'] or
has_perm(self.request.user, 'motions.can_manage'))) has_perm(self.request.user, 'motions.can_manage')))
elif self.action in ('manage_version', 'set_state', 'set_recommendation', elif self.action in ('manage_version', 'set_state', 'set_recommendation',
'follow_recommendation', 'create_poll'): 'follow_recommendation', 'create_poll', 'manage_submitters',
'sort_submitters'):
result = (has_perm(self.request.user, 'motions.can_see') and result = (has_perm(self.request.user, 'motions.can_see') and
has_perm(self.request.user, 'motions.can_manage')) has_perm(self.request.user, 'motions.can_manage'))
elif self.action == 'support': elif self.action == 'support':
@ -150,6 +155,38 @@ class MotionViewSet(ModelViewSet):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
motion = serializer.save(request_user=request.user) motion = serializer.save(request_user=request.user)
# Check for submitters and make ids unique
if isinstance(request.data, QueryDict):
submitters_id = request.data.getlist('submitters_id')
else:
submitters_id = request.data.get('submitters_id')
if submitters_id is None:
submitters_id = []
if not isinstance(submitters_id, list):
raise ValidationError({'detail': _('If submitters_id is given, it has to be a list.')})
submitters_id_unique = set()
for id in submitters_id:
try:
submitters_id_unique.add(int(id))
except ValueError:
continue
submitters = []
for submitter_id in submitters_id_unique:
try:
submitters.append(get_user_model().objects.get(pk=submitter_id))
except get_user_model().DoesNotExist:
continue # Do not add users that do not exist
# Add the request user, if he is authenticated and no submitters were given:
if len(submitters) == 0 and request.user.is_authenticated():
submitters.append(request.user)
# create all submitters
for submitter in submitters:
Submitter.objects.add(submitter, motion)
# Write the log message and initiate response. # Write the log message and initiate response.
motion.write_log([ugettext_noop('Motion created')], request.user) motion.write_log([ugettext_noop('Motion created')], request.user)
@ -264,10 +301,9 @@ class MotionViewSet(ModelViewSet):
[ugettext_noop('Comment {} updated').format(', '.join(changed_comment_fields))], [ugettext_noop('Comment {} updated').format(', '.join(changed_comment_fields))],
request.user) request.user)
# Send new submitters and supporters via autoupdate because users # Send new supporters via autoupdate because users
# without permission to see users may not have them but can get it now. # without permission to see users may not have them but can get it now.
new_users = list(updated_motion.submitters.all()) new_users = list(updated_motion.supporters.all())
new_users.extend(updated_motion.supporters.all())
inform_changed_data(new_users) inform_changed_data(new_users)
return Response(serializer.data) return Response(serializer.data)
@ -316,6 +352,106 @@ class MotionViewSet(ModelViewSet):
# Initiate response. # Initiate response.
return Response({'detail': message}) return Response({'detail': message})
@detail_route(methods=['POST', 'DELETE'])
def manage_submitters(self, request, pk=None):
"""
POST: Add a user as a submitter to this motion.
DELETE: Remove the user as a submitter from this motion.
For both cases provide ['user': <user_id>} for the user to add or remove.
"""
motion = self.get_object()
if request.method == 'POST':
user_id = request.data.get('user')
# Check permissions and other conditions. Get user instance.
if user_id is None:
raise ValidationError({'detail': _('You have to provide a user.')})
else:
try:
user = get_user_model().objects.get(pk=int(user_id))
except (ValueError, get_user_model().DoesNotExist):
raise ValidationError({'detail': _('User does not exist.')})
# Try to add the user. This ensurse that a user is not twice a submitter
try:
Submitter.objects.add(user, motion)
except OpenSlidesError as e:
raise ValidationError({'detail': str(e)})
message = _('User %s was successfully added as a submitter.') % user
# Send new submitter via autoupdate because users without permission
# to see users may not have it but can get it now.
inform_changed_data(user)
else: # DELETE
user_id = request.data.get('user')
# Check permissions and other conditions. Get user instance.
if user_id is None:
raise ValidationError({'detail': _('You have to provide a user.')})
else:
try:
user = get_user_model().objects.get(pk=int(user_id))
except (ValueError, get_user_model().DoesNotExist):
raise ValidationError({'detail': _('User does not exist.')})
queryset = Submitter.objects.filter(motion=motion, user=user)
try:
# We assume that there aren't multiple entries because this
# is forbidden by the Manager's add method. We assume that
# there is only one submitter instance or none.
submitter = queryset.get()
except Submitter.DoesNotExist:
raise ValidationError({'detail': _('The user is not a submitter.')})
else:
name = str(submitter.user)
submitter.delete()
message = _('User {} successfully removed as a submitter.').format(name)
# Initiate response.
return Response({'detail': message})
@detail_route(methods=['POST'])
def sort_submitters(self, request, pk=None):
"""
Special view endpoint to sort the submitters.
Send {'submitters': [<submitter_id_1>, <submitter_id_2>, ...]} as payload.
"""
# Retrieve motion.
motion = self.get_object()
# Check data
submitter_ids = request.data.get('submitters')
if not isinstance(submitter_ids, list):
raise ValidationError(
{'detail': _('Invalid data.')})
# Get all submitters
submitters = {}
for submitter in motion.submitters.all():
submitters[submitter.pk] = submitter
# Check and sort submitters
valid_submitters = []
for submitter_id in submitter_ids:
if not isinstance(submitter_id, int) or submitters.get(submitter_id) is None:
raise ValidationError(
{'detail': _('Invalid data.')})
valid_submitters.append(submitters[submitter_id])
weight = 1
with transaction.atomic():
for submitter in valid_submitters:
submitter.weight = weight
submitter.save(skip_autoupdate=True)
weight += 1
# send autoupdate
inform_changed_data(motion)
# Initiate response.
return Response({'detail': _('Submitters successfully sorted.')})
@detail_route(methods=['post', 'delete']) @detail_route(methods=['post', 'delete'])
def support(self, request, pk=None): def support(self, request, pk=None):
""" """

View File

@ -15,6 +15,7 @@ from openslides.motions.models import (
MotionBlock, MotionBlock,
MotionLog, MotionLog,
State, State,
Submitter,
Workflow, Workflow,
) )
from openslides.users.models import Group from openslides.users.models import Group
@ -175,7 +176,7 @@ class CreateMotion(TestCase):
self.assertEqual(motion.title, 'test_title_OoCoo3MeiT9li5Iengu9') self.assertEqual(motion.title, 'test_title_OoCoo3MeiT9li5Iengu9')
self.assertEqual(motion.identifier, '1') self.assertEqual(motion.identifier, '1')
self.assertTrue(motion.submitters.exists()) self.assertTrue(motion.submitters.exists())
self.assertEqual(motion.submitters.get().username, 'admin') self.assertEqual(motion.submitters.get().user.username, 'admin')
def test_with_reason(self): def test_with_reason(self):
response = self.client.post( response = self.client.post(
@ -467,14 +468,15 @@ class RetrieveMotion(TestCase):
user = get_user_model().objects.create_user( user = get_user_model().objects.create_user(
username='username_ohS2opheikaSa5theijo', username='username_ohS2opheikaSa5theijo',
password='password_kau4eequaisheeBateef') password='password_kau4eequaisheeBateef')
self.motion.submitters.add(user) Submitter.objects.add(user, self.motion)
submitter_client = APIClient() submitter_client = APIClient()
submitter_client.force_login(user) submitter_client.force_login(user)
response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk])) response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data(self): def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data(self):
self.motion.submitters.add(get_user_model().objects.get(username='admin')) admin = get_user_model().objects.get(username='admin')
Submitter.objects.add(admin, self.motion)
group = Group.objects.get(pk=1) # Group with pk 1 is for anonymous and default users. group = Group.objects.get(pk=1) # Group with pk 1 is for anonymous and default users.
permission_string = 'users.can_see_name' permission_string = 'users.can_see_name'
app_label, codename = permission_string.split('.') app_label, codename = permission_string.split('.')
@ -486,7 +488,8 @@ class RetrieveMotion(TestCase):
response_1 = guest_client.get(reverse('motion-detail', args=[self.motion.pk])) response_1 = guest_client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response_1.status_code, status.HTTP_200_OK) self.assertEqual(response_1.status_code, status.HTTP_200_OK)
response_2 = guest_client.get(reverse('user-detail', args=[response_1.data['submitters_id'][0]])) submitter_id = response_1.data['submitters'][0]['user_id']
response_2 = guest_client.get(reverse('user-detail', args=[submitter_id]))
self.assertEqual(response_2.status_code, status.HTTP_200_OK) self.assertEqual(response_2.status_code, status.HTTP_200_OK)
extra_user = get_user_model().objects.create_user( extra_user = get_user_model().objects.create_user(
@ -552,7 +555,7 @@ class UpdateMotion(TestCase):
username='test_username_uqu6PhoovieB9eicah0o', username='test_username_uqu6PhoovieB9eicah0o',
password='test_password_Xaesh8ohg6CoheTe3awo') password='test_password_Xaesh8ohg6CoheTe3awo')
motion = Motion.objects.get() motion = Motion.objects.get()
motion.submitters.add(non_admin) Submitter.objects.add(non_admin, self.motion)
motion.supporters.clear() motion.supporters.clear()
response = self.client.patch( response = self.client.patch(
reverse('motion-detail', args=[self.motion.pk]), reverse('motion-detail', args=[self.motion.pk]),
@ -566,7 +569,7 @@ class UpdateMotion(TestCase):
admin = get_user_model().objects.get(username='admin') admin = get_user_model().objects.get(username='admin')
group_admin = admin.groups.get(name='Admin') group_admin = admin.groups.get(name='Admin')
admin.groups.remove(group_admin) admin.groups.remove(group_admin)
self.motion.submitters.add(admin) Submitter.objects.add(admin, self.motion)
supporter = get_user_model().objects.create_user( supporter = get_user_model().objects.create_user(
username='test_username_ahshi4oZin0OoSh9chee', username='test_username_ahshi4oZin0OoSh9chee',
password='test_password_Sia8ahgeenixu5cei2Ib') password='test_password_Sia8ahgeenixu5cei2Ib')
@ -678,7 +681,7 @@ class DeleteMotion(TestCase):
def test_delete_own_motion_as_delegate(self): def test_delete_own_motion_as_delegate(self):
self.make_admin_delegate() self.make_admin_delegate()
self.put_motion_in_complex_workflow() self.put_motion_in_complex_workflow()
self.motion.submitters.add(self.admin) Submitter.objects.add(self.admin, self.motion)
response = self.client.delete( response = self.client.delete(
reverse('motion-detail', args=[self.motion.pk])) reverse('motion-detail', args=[self.motion.pk]))
@ -732,6 +735,118 @@ class ManageVersion(TestCase):
self.assertEqual(response.data, {'detail': 'You can not delete the active version of a motion.'}) self.assertEqual(response.data, {'detail': 'You can not delete the active version of a motion.'})
class ManageSubmitters(TestCase):
"""
Tests adding and removing of submitters.
"""
def setUp(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
self.admin = get_user_model().objects.get()
self.motion = Motion(
title='test_title_SlqfMw(waso0saWMPqcZ',
text='test_text_f30skclqS9wWF=xdfaSL')
self.motion.save()
def test_add_existing_user(self):
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.motion.submitters.count(), 1)
def test_add_non_existing_user(self):
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': 1337})
self.assertEqual(response.status_code, 400)
self.assertEqual(self.motion.submitters.count(), 0)
def test_add_user_twice(self):
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
self.assertEqual(response.status_code, 400)
self.assertEqual(self.motion.submitters.count(), 1)
def test_add_user_no_data(self):
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]))
self.assertEqual(response.status_code, 400)
self.assertEqual(self.motion.submitters.count(), 0)
def test_add_user_invalid_data(self):
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': ['invalid_str']})
self.assertEqual(response.status_code, 400)
self.assertEqual(self.motion.submitters.count(), 0)
def test_add_without_permission(self):
admin = get_user_model().objects.get(username='admin')
group_admin = admin.groups.get(name='Admin')
group_delegates = type(group_admin).objects.get(name='Delegates')
admin.groups.add(group_delegates)
admin.groups.remove(group_admin)
CollectionElement.from_instance(admin)
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
self.assertEqual(response.status_code, 403)
self.assertEqual(self.motion.submitters.count(), 0)
def test_remove_existing_user(self):
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
response = self.client.delete(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
self.assertEqual(response.status_code, 200)
self.assertEqual(self.motion.submitters.count(), 0)
def test_remove_non_existing_user(self):
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
response = self.client.delete(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': 1337})
self.assertEqual(response.status_code, 400)
self.assertEqual(self.motion.submitters.count(), 1)
def test_remove_existing_user_twice(self):
response = self.client.post(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
response = self.client.delete(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
response = self.client.delete(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': self.admin.pk})
self.assertEqual(response.status_code, 400)
self.assertEqual(self.motion.submitters.count(), 0)
def test_remove_user_no_data(self):
response = self.client.delete(
reverse('motion-manage-submitters', args=[self.motion.pk]))
self.assertEqual(response.status_code, 400)
self.assertEqual(self.motion.submitters.count(), 0)
def test_remove_user_invalid_data(self):
response = self.client.delete(
reverse('motion-manage-submitters', args=[self.motion.pk]),
{'user': ['invalid_str']})
self.assertEqual(response.status_code, 400)
self.assertEqual(self.motion.submitters.count(), 0)
class CreateMotionChangeRecommendation(TestCase): class CreateMotionChangeRecommendation(TestCase):
""" """
Tests motion change recommendation creation. Tests motion change recommendation creation.

View File

@ -11,6 +11,7 @@ class MotionViewSetCreate(TestCase):
def setUp(self): def setUp(self):
self.request = MagicMock() self.request = MagicMock()
self.request.data.get.return_value = None self.request.data.get.return_value = None
self.request.user.is_authenticated.return_value = False
self.view_instance = MotionViewSet() self.view_instance = MotionViewSet()
self.view_instance.request = self.request self.view_instance.request = self.request
self.view_instance.format_kwarg = MagicMock() self.view_instance.format_kwarg = MagicMock()
@ -21,7 +22,6 @@ class MotionViewSetCreate(TestCase):
@patch('openslides.motions.views.has_perm') @patch('openslides.motions.views.has_perm')
@patch('openslides.motions.views.config') @patch('openslides.motions.views.config')
def test_simple_create(self, mock_config, mock_has_perm, mock_icd): def test_simple_create(self, mock_config, mock_has_perm, mock_icd):
self.request.user = 1
mock_has_perm.return_value = True mock_has_perm.return_value = True
self.view_instance.create(self.request) self.view_instance.create(self.request)