Sort submitters
This commit is contained in:
parent
6b2a736a6c
commit
b0a42e19e1
@ -27,7 +27,7 @@ script:
|
||||
- node_modules/.bin/karma start --browsers PhantomJS tests/karma/karma.conf.js
|
||||
|
||||
- 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
|
||||
- coverage report --fail-under=73
|
||||
|
@ -8,7 +8,8 @@ Version 2.3 (unreleased)
|
||||
========================
|
||||
|
||||
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)
|
||||
|
@ -1,26 +1,26 @@
|
||||
<div ng-if="item" class="details" ng-controller="ListOfSpeakersManagementCtrl">
|
||||
<div class="speakers-toolbar">
|
||||
<div class="pull-right">
|
||||
<span os-perms="agenda.can_manage">
|
||||
<button ng-if="item.speaker_list_closed" ng-click="closeList(false)"
|
||||
 class="btn btn-sm btn-default">
|
||||
<translate>Open list of speakers</translate>
|
||||
 </button>
|
||||
 <button ng-if="!item.speaker_list_closed" ng-click="closeList(true)"
|
||||
 class="btn btn-sm btn-default">
|
||||
 <translate>Close list of speakers</translate>
|
||||
 </button>
|
||||
</span>
|
||||
<span os-perms="agenda.can_manage_list_of_speakers">
|
||||
<button ng-if="isAllowed('removeAll')" class="btn btn-sm btn-danger"
|
||||
<div class="speakers-toolbar">
|
||||
<div class="pull-right">
|
||||
<span os-perms="agenda.can_manage">
|
||||
<button ng-if="item.speaker_list_closed" ng-click="closeList(false)"
|
||||
class="btn btn-sm btn-default">
|
||||
<translate>Open list of speakers</translate>
|
||||
</button>
|
||||
<button ng-if="!item.speaker_list_closed" ng-click="closeList(true)"
|
||||
class="btn btn-sm btn-default">
|
||||
<translate>Close list of speakers</translate>
|
||||
</button>
|
||||
</span>
|
||||
<span os-perms="agenda.can_manage_list_of_speakers">
|
||||
<button ng-if="isAllowed('removeAll')" class="btn btn-sm btn-danger"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to remove all speakers from this list?'| translate }}"
|
||||
ng-bootbox-confirm-action="removeAllSpeakers()">
|
||||
<i class="fa fa-trash fa-lg"></i>
|
||||
<translate>Remove all speakers</translate>
|
||||
</button>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- text for empty list -->
|
||||
<p ng-if="speakers.length == 0" translate>
|
||||
@ -79,7 +79,7 @@
|
||||
<div ng-show="nextSpeakers.length > 0">
|
||||
<div ui-tree="treeOptions" data-empty-placeholder-enabled="false">
|
||||
<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>
|
||||
{{ $index + 1 }}.
|
||||
{{ speaker.user.get_full_name() }}
|
||||
|
@ -274,11 +274,8 @@ strong, b, th {
|
||||
|
||||
|
||||
.meta {
|
||||
h3 {
|
||||
font-family: $font-condensed-light;
|
||||
}
|
||||
|
||||
.heading, h3 {
|
||||
font-family: $font-condensed-light;
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
font-weight: 300;
|
||||
|
@ -43,7 +43,8 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||
for full in full_data:
|
||||
# Check if user is submitter of this motion.
|
||||
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:
|
||||
# Anonymous users can not be submitters.
|
||||
is_submitter = False
|
||||
|
72
openslides/motions/migrations/0006_submitter_model.py
Normal file
72
openslides/motions/migrations/0006_submitter_model.py
Normal 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',
|
||||
),
|
||||
]
|
@ -1,6 +1,7 @@
|
||||
from typing import Any, Dict # noqa
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.db import IntegrityError, models, transaction
|
||||
@ -21,6 +22,7 @@ from openslides.poll.models import (
|
||||
CollectDefaultVotesMixin,
|
||||
)
|
||||
from openslides.utils.autoupdate import inform_changed_data
|
||||
from openslides.utils.exceptions import OpenSlidesError
|
||||
from openslides.utils.models import RESTModelMixin
|
||||
|
||||
from .access_permissions import (
|
||||
@ -157,11 +159,6 @@ class Motion(RESTModelMixin, models.Model):
|
||||
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)
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
return user in self.submitters.all()
|
||||
return self.submitters.filter(user=user).exists()
|
||||
|
||||
def is_supporter(self, user):
|
||||
"""
|
||||
@ -706,6 +703,69 @@ class Motion(RESTModelMixin, models.Model):
|
||||
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):
|
||||
"""
|
||||
A MotionVersion object saves some date of the motion.
|
||||
|
@ -29,7 +29,8 @@ class MotionSlide(ProjectorElement):
|
||||
yield motion.agenda_item
|
||||
yield motion.state.workflow
|
||||
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 MotionChangeRecommendation.objects.filter(motion_version=motion.get_active_version().id)
|
||||
|
||||
|
@ -24,6 +24,7 @@ from .models import (
|
||||
MotionPoll,
|
||||
MotionVersion,
|
||||
State,
|
||||
Submitter,
|
||||
Workflow,
|
||||
)
|
||||
|
||||
@ -290,6 +291,20 @@ class MotionChangeRecommendationSerializer(ModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class SubmitterSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Submitter objects.
|
||||
"""
|
||||
class Meta:
|
||||
model = Submitter
|
||||
fields = (
|
||||
'id',
|
||||
'user',
|
||||
'motion',
|
||||
'weight',
|
||||
)
|
||||
|
||||
|
||||
class MotionSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Motion objects.
|
||||
@ -310,6 +325,7 @@ class MotionSerializer(ModelSerializer):
|
||||
write_only=True)
|
||||
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)
|
||||
submitters = SubmitterSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
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['parent_id'] = validated_data.get('agenda_parent_id')
|
||||
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.attachments.add(*validated_data.get('attachments', []))
|
||||
motion.tags.add(*validated_data.get('tags', []))
|
||||
|
@ -130,6 +130,7 @@ def required_users(sender, request_user, **kwargs):
|
||||
if has_perm(request_user, 'motions.can_see'):
|
||||
for motion_collection_element in Collection(Motion.get_collection_string()).element_generator():
|
||||
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'])
|
||||
return submitters_supporters
|
||||
|
@ -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', [
|
||||
'DS',
|
||||
@ -571,6 +587,7 @@ angular.module('OpenSlidesApp.motions', [
|
||||
* There are the following possible actions.
|
||||
* - see
|
||||
* - update
|
||||
* - update_submitters
|
||||
* - delete
|
||||
* - create_poll
|
||||
* - support
|
||||
@ -604,6 +621,8 @@ angular.module('OpenSlidesApp.motions', [
|
||||
this.state.allow_submitter_edit
|
||||
)
|
||||
);
|
||||
case 'update_submitters':
|
||||
return operator.hasPerms('motions.can_manage');
|
||||
case 'delete':
|
||||
return (
|
||||
operator.hasPerms('motions.can_manage') ||
|
||||
@ -755,20 +774,18 @@ angular.module('OpenSlidesApp.motions', [
|
||||
localField: 'attachments',
|
||||
localKeys: 'attachments_id',
|
||||
},
|
||||
'users/user': [
|
||||
{
|
||||
localField: 'submitters',
|
||||
localKeys: 'submitters_id',
|
||||
},
|
||||
{
|
||||
localField: 'supporters',
|
||||
localKeys: 'supporters_id',
|
||||
}
|
||||
],
|
||||
'users/user': {
|
||||
localField: 'supporters',
|
||||
localKeys: 'supporters_id',
|
||||
},
|
||||
'motions/motion-poll': {
|
||||
localField: 'polls',
|
||||
foreignKey: 'motion_id',
|
||||
}
|
||||
},
|
||||
'motions/submitter': {
|
||||
localField: 'submitters',
|
||||
foreignKey: 'motion_id',
|
||||
},
|
||||
},
|
||||
hasOne: {
|
||||
'motions/workflowstate': [
|
||||
@ -987,7 +1004,8 @@ angular.module('OpenSlidesApp.motions', [
|
||||
'Category',
|
||||
'Workflow',
|
||||
'MotionChangeRecommendation',
|
||||
function(Motion, Category, Workflow, MotionChangeRecommendation) {}
|
||||
'Submitter',
|
||||
function(Motion, Category, Workflow, MotionChangeRecommendation, Submitter) {}
|
||||
])
|
||||
|
||||
|
||||
|
@ -5,11 +5,12 @@
|
||||
angular.module('OpenSlidesApp.motions.csv', [])
|
||||
|
||||
.factory('MotionCsvExport', [
|
||||
'$filter',
|
||||
'gettextCatalog',
|
||||
'Config',
|
||||
'CsvDownload',
|
||||
'lineNumberingService',
|
||||
function (gettextCatalog, Config, CsvDownload, lineNumberingService) {
|
||||
function ($filter, gettextCatalog, Config, CsvDownload, lineNumberingService) {
|
||||
var makeHeaderline = function (params) {
|
||||
var headerline = ['Identifier', 'Title'];
|
||||
if (params.include.text) {
|
||||
@ -77,8 +78,12 @@ angular.module('OpenSlidesApp.motions.csv', [])
|
||||
// Submitters
|
||||
if (params.include.submitters) {
|
||||
var submitters = [];
|
||||
angular.forEach(motion.submitters, function(user) {
|
||||
var user_short_name = [user.title, user.first_name, user.last_name].join(' ').trim();
|
||||
_.forEach($filter('orderBy')(motion.submitters, 'weight'), function (user) {
|
||||
var user_short_name = [
|
||||
user.user.title,
|
||||
user.user.first_name,
|
||||
user.user.last_name
|
||||
].join(' ').trim();
|
||||
submitters.push(user_short_name);
|
||||
});
|
||||
row.push('"' + submitters.join('; ') + '"');
|
||||
|
@ -7,6 +7,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
|
||||
.factory('MotionDocxExport', [
|
||||
'$http',
|
||||
'$q',
|
||||
'$filter',
|
||||
'operator',
|
||||
'Config',
|
||||
'Category',
|
||||
@ -15,8 +16,8 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
|
||||
'lineNumberingService',
|
||||
'Html2DocxConverter',
|
||||
'MotionComment',
|
||||
function ($http, $q, operator, Config, Category, gettextCatalog, FileSaver, lineNumberingService,
|
||||
Html2DocxConverter, MotionComment) {
|
||||
function ($http, $q, $filter, operator, Config, Category, gettextCatalog,
|
||||
FileSaver, lineNumberingService, Html2DocxConverter, MotionComment) {
|
||||
|
||||
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,
|
||||
identifier: motion.identifier || '',
|
||||
title: title,
|
||||
submitters: params.include.submitters ? _.map(motion.submitters, function (submitter) {
|
||||
return submitter.get_full_name();
|
||||
}).join(', ') : '',
|
||||
submitters: params.include.submitters ? _.map(
|
||||
$filter('orderBy')(motion.submitters, 'weight'), function (submitter) {
|
||||
return submitter.user.get_full_name();
|
||||
}
|
||||
).join(', ') : '',
|
||||
status: motion.getStateName(),
|
||||
// Miscellaneous stuff
|
||||
preamble: gettextCatalog.getString(Config.get('motions_preamble').value),
|
||||
|
@ -6,6 +6,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
|
||||
.factory('MotionContentProvider', [
|
||||
'$q',
|
||||
'$filter',
|
||||
'operator',
|
||||
'gettextCatalog',
|
||||
'PDFLayout',
|
||||
@ -17,7 +18,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
'Motion',
|
||||
'MotionComment',
|
||||
'OpenSlidesSettings',
|
||||
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter,
|
||||
function($q, $filter, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter,
|
||||
HTMLValidizer, Category, Config, Motion, MotionComment, OpenSlidesSettings) {
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
// submitters
|
||||
var submitters = _.map(motion.submitters, function (submitter) {
|
||||
return submitter.get_full_name();
|
||||
}).join(', ');
|
||||
var submitters = _.map(
|
||||
$filter('orderBy')(motion.submitters, 'weight'), function (submitter) {
|
||||
return submitter.user.get_full_name();
|
||||
}
|
||||
).join(', ');
|
||||
if (params.include.submitters) {
|
||||
metaTableBody.push([
|
||||
{
|
||||
|
@ -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', {
|
||||
url: '/import',
|
||||
controller: 'MotionImportCtrl',
|
||||
@ -388,67 +401,73 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
getFormFields: function (isCreateForm) {
|
||||
var workflows = Workflow.getAll();
|
||||
var images = Mediafile.getAllImages();
|
||||
var formFields = [
|
||||
{
|
||||
var formFields = [];
|
||||
formFields.push({
|
||||
key: 'identifier',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Identifier')
|
||||
},
|
||||
hide: true
|
||||
},
|
||||
{
|
||||
key: 'submitters_id',
|
||||
type: 'select-multiple',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Submitters'),
|
||||
options: User.getAll(),
|
||||
ngOptions: 'option.id as option.full_name for option in to.options',
|
||||
placeholder: gettextCatalog.getString('Select or search a submitter ...')
|
||||
});
|
||||
|
||||
if (isCreateForm) {
|
||||
formFields.push({
|
||||
key: 'submitters_id',
|
||||
type: 'select-multiple',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Submitters'),
|
||||
options: User.getAll(),
|
||||
ngOptions: 'option.id as option.full_name for option in to.options',
|
||||
placeholder: gettextCatalog.getString('Select or search a submitter ...'),
|
||||
},
|
||||
hide: !operator.hasPerms('motions.can_manage')
|
||||
});
|
||||
}
|
||||
|
||||
formFields = formFields.concat([
|
||||
{
|
||||
key: 'title',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Title'),
|
||||
required: true
|
||||
}
|
||||
},
|
||||
hide: !operator.hasPerms('motions.can_manage')
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Title'),
|
||||
required: true
|
||||
{
|
||||
template: '<p class="spacer-top-lg no-padding">' + Config.translate(Config.get('motions_preamble').value) + '</p>'
|
||||
},
|
||||
{
|
||||
key: 'text',
|
||||
type: 'editor',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Text'),
|
||||
required: true
|
||||
},
|
||||
data: {
|
||||
ckeditorOptions: Editor.getOptions()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'reason',
|
||||
type: 'editor',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Reason'),
|
||||
},
|
||||
data: {
|
||||
ckeditorOptions: Editor.getOptions()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'disable_versioning',
|
||||
type: 'checkbox',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Trivial change'),
|
||||
description: gettextCatalog.getString("Don't create a new version.")
|
||||
},
|
||||
hide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
template: '<p class="spacer-top-lg no-padding">' + Config.translate(Config.get('motions_preamble').value) + '</p>'
|
||||
},
|
||||
{
|
||||
key: 'text',
|
||||
type: 'editor',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Text'),
|
||||
required: true
|
||||
},
|
||||
data: {
|
||||
ckeditorOptions: Editor.getOptions()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'reason',
|
||||
type: 'editor',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Reason'),
|
||||
},
|
||||
data: {
|
||||
ckeditorOptions: Editor.getOptions()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'disable_versioning',
|
||||
type: 'checkbox',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Trivial change'),
|
||||
description: gettextCatalog.getString("Don't create a new version.")
|
||||
},
|
||||
hide: true
|
||||
}];
|
||||
]);
|
||||
|
||||
// show as agenda item + parent item
|
||||
if (isCreateForm) {
|
||||
@ -1192,7 +1211,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
];
|
||||
$scope.filter.propertyDict = {
|
||||
'submitters': function (submitter) {
|
||||
return submitter.get_short_name();
|
||||
return submitter.user.get_short_name();
|
||||
},
|
||||
'supporters': function (supporter) {
|
||||
return supporter.get_short_name();
|
||||
@ -1235,7 +1254,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
display_name: gettext('Identifier')},
|
||||
{name: 'getTitle()',
|
||||
display_name: gettext('Title')},
|
||||
{name: 'submitters[0].get_short_name()',
|
||||
{name: 'submitters[0].user.get_short_name()',
|
||||
display_name: gettext('Submitters')},
|
||||
{name: 'category.' + Config.get('motions_export_category_sorting').value,
|
||||
display_name: gettext('Category')},
|
||||
@ -2117,7 +2136,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'operator',
|
||||
'ErrorMessage',
|
||||
'EditingWarning',
|
||||
function($scope, $state, Motion, Category, Config, Mediafile, MotionForm,
|
||||
function ($scope, $state, Motion, Category, Config, Mediafile, MotionForm,
|
||||
Tag, User, Workflow, Agenda, motionId, operator, ErrorMessage,
|
||||
EditingWarning) {
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
@ -2176,7 +2195,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
Motion.inject(motion);
|
||||
// save changed motion object on server
|
||||
Motion.save(motion).then(
|
||||
function(success) {
|
||||
function (success) {
|
||||
if (gotoDetailView) {
|
||||
$state.go('motions.motion.detail', {id: success.id});
|
||||
}
|
||||
@ -2201,7 +2220,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'motionpollId',
|
||||
'voteNumber',
|
||||
'ErrorMessage',
|
||||
function($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId,
|
||||
function ($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId,
|
||||
voteNumber, ErrorMessage) {
|
||||
// set initial values for form model by create deep copy of motionpoll object
|
||||
// so detail view is not updated while editing poll
|
||||
@ -2220,16 +2239,76 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
votesinvalid: poll.votesinvalid,
|
||||
votescast: poll.votescast
|
||||
})
|
||||
.then(function(success) {
|
||||
.then(function (success) {
|
||||
$scope.alert.show = false;
|
||||
$scope.closeThisDialog();
|
||||
}, function(error) {
|
||||
}, function (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', [
|
||||
'$scope',
|
||||
'$q',
|
||||
@ -2239,7 +2318,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'MotionBlock',
|
||||
'User',
|
||||
'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
|
||||
$scope.motions = [];
|
||||
|
||||
@ -2295,7 +2374,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
}
|
||||
// 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();
|
||||
if (user_short_name == motion.submitter.trim()) {
|
||||
motion.submitters_id = [user.id];
|
||||
|
@ -125,9 +125,15 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<!-- submitters -->
|
||||
<h3 translate>Submitters</h3>
|
||||
<div ng-repeat="submitter in motion.submitters">
|
||||
{{ submitter.get_full_name() }}
|
||||
<h3 ng-if="!motion.isAllowed('update_submitters')" class="heading" translate>Submitters</h3>
|
||||
<h3 ng-if="motion.isAllowed('update_submitters')" class="heading">
|
||||
<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>
|
||||
|
||||
<!-- supporters -->
|
||||
@ -211,7 +217,7 @@
|
||||
<div ng-if="motion.isAllowed('change_recommendation')" class="heading">
|
||||
<span uib-dropdown>
|
||||
<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>
|
||||
</span>
|
||||
<ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="recommendation-dropdown">
|
||||
|
@ -628,8 +628,8 @@
|
||||
<div ng-if="motion.submitters.length">
|
||||
<small>
|
||||
<span class="optional" translate>by</span>
|
||||
<span class="optional" ng-repeat="submitter in motion.submitters | limitTo:1">
|
||||
{{ submitter.get_full_name() }}<span ng-if="!$last">,</span></span><span ng-if="motion.submitters.length > 1">,
|
||||
<span class="optional" ng-repeat="submitter in motion.submitters | orderBy: 'weight' | limitTo:1">
|
||||
{{ submitter.user.get_full_name() }}<span ng-if="!$last">,</span></span><span ng-if="motion.submitters.length > 1">,
|
||||
... [+{{ motion.submitters.length - 1 }}]</span>
|
||||
<!-- sorry for merging them together, but otherwise there would be a whitespace because of the new line -->
|
||||
</small>
|
||||
@ -640,6 +640,11 @@
|
||||
<span ng-if="motion.isAllowed('update')">
|
||||
<a href="" ng-click="openDialog(motion)" translate>Edit</a>
|
||||
</span>
|
||||
<span ng-if="motion.isAllowed('update_submitters')"> ·
|
||||
<a ui-sref="motions.motion.submitters({id: motion.id})" translate>
|
||||
Edit submitters
|
||||
</a>
|
||||
</span>
|
||||
<span ng-if="motion.isAllowed('delete')"> ·
|
||||
<a href="" class="text-danger"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br><b>{{ motion.getTitle() }}</b>"
|
||||
|
@ -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>
|
@ -12,8 +12,8 @@
|
||||
|
||||
<!-- Submitters -->
|
||||
<h3 ng-if="motion.submitters.length > 0" translate>Submitters</h3>
|
||||
<div ng-repeat="submitter in motion.submitters">
|
||||
{{ submitter.get_full_name() }}<br>
|
||||
<div ng-repeat="submitter in motion.submitters | orderBy: 'weight'">
|
||||
{{ submitter.user.get_full_name() }}<span ng-hide="$last">,</span>
|
||||
</div>
|
||||
|
||||
<!-- Poll results -->
|
||||
|
@ -2,9 +2,11 @@ import re
|
||||
from typing import Optional # noqa
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.http import Http404
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_noop
|
||||
from rest_framework import status
|
||||
@ -13,6 +15,7 @@ from ..core.config import config
|
||||
from ..utils.auth import has_perm
|
||||
from ..utils.autoupdate import inform_changed_data
|
||||
from ..utils.collection import CollectionElement
|
||||
from ..utils.exceptions import OpenSlidesError
|
||||
from ..utils.rest_api import (
|
||||
DestroyModelMixin,
|
||||
GenericViewSet,
|
||||
@ -39,6 +42,7 @@ from .models import (
|
||||
MotionPoll,
|
||||
MotionVersion,
|
||||
State,
|
||||
Submitter,
|
||||
Workflow,
|
||||
)
|
||||
from .serializers import MotionPollSerializer
|
||||
@ -73,7 +77,8 @@ class MotionViewSet(ModelViewSet):
|
||||
(not config['motions_stop_submitting'] or
|
||||
has_perm(self.request.user, 'motions.can_manage')))
|
||||
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
|
||||
has_perm(self.request.user, 'motions.can_manage'))
|
||||
elif self.action == 'support':
|
||||
@ -150,6 +155,38 @@ class MotionViewSet(ModelViewSet):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
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.
|
||||
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))],
|
||||
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.
|
||||
new_users = list(updated_motion.submitters.all())
|
||||
new_users.extend(updated_motion.supporters.all())
|
||||
new_users = list(updated_motion.supporters.all())
|
||||
inform_changed_data(new_users)
|
||||
|
||||
return Response(serializer.data)
|
||||
@ -316,6 +352,106 @@ class MotionViewSet(ModelViewSet):
|
||||
# Initiate response.
|
||||
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'])
|
||||
def support(self, request, pk=None):
|
||||
"""
|
||||
|
@ -15,6 +15,7 @@ from openslides.motions.models import (
|
||||
MotionBlock,
|
||||
MotionLog,
|
||||
State,
|
||||
Submitter,
|
||||
Workflow,
|
||||
)
|
||||
from openslides.users.models import Group
|
||||
@ -175,7 +176,7 @@ class CreateMotion(TestCase):
|
||||
self.assertEqual(motion.title, 'test_title_OoCoo3MeiT9li5Iengu9')
|
||||
self.assertEqual(motion.identifier, '1')
|
||||
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):
|
||||
response = self.client.post(
|
||||
@ -467,14 +468,15 @@ class RetrieveMotion(TestCase):
|
||||
user = get_user_model().objects.create_user(
|
||||
username='username_ohS2opheikaSa5theijo',
|
||||
password='password_kau4eequaisheeBateef')
|
||||
self.motion.submitters.add(user)
|
||||
Submitter.objects.add(user, self.motion)
|
||||
submitter_client = APIClient()
|
||||
submitter_client.force_login(user)
|
||||
response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk]))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
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.
|
||||
permission_string = 'users.can_see_name'
|
||||
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]))
|
||||
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)
|
||||
|
||||
extra_user = get_user_model().objects.create_user(
|
||||
@ -552,7 +555,7 @@ class UpdateMotion(TestCase):
|
||||
username='test_username_uqu6PhoovieB9eicah0o',
|
||||
password='test_password_Xaesh8ohg6CoheTe3awo')
|
||||
motion = Motion.objects.get()
|
||||
motion.submitters.add(non_admin)
|
||||
Submitter.objects.add(non_admin, self.motion)
|
||||
motion.supporters.clear()
|
||||
response = self.client.patch(
|
||||
reverse('motion-detail', args=[self.motion.pk]),
|
||||
@ -566,7 +569,7 @@ class UpdateMotion(TestCase):
|
||||
admin = get_user_model().objects.get(username='admin')
|
||||
group_admin = admin.groups.get(name='Admin')
|
||||
admin.groups.remove(group_admin)
|
||||
self.motion.submitters.add(admin)
|
||||
Submitter.objects.add(admin, self.motion)
|
||||
supporter = get_user_model().objects.create_user(
|
||||
username='test_username_ahshi4oZin0OoSh9chee',
|
||||
password='test_password_Sia8ahgeenixu5cei2Ib')
|
||||
@ -678,7 +681,7 @@ class DeleteMotion(TestCase):
|
||||
def test_delete_own_motion_as_delegate(self):
|
||||
self.make_admin_delegate()
|
||||
self.put_motion_in_complex_workflow()
|
||||
self.motion.submitters.add(self.admin)
|
||||
Submitter.objects.add(self.admin, self.motion)
|
||||
|
||||
response = self.client.delete(
|
||||
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.'})
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Tests motion change recommendation creation.
|
||||
|
@ -11,6 +11,7 @@ class MotionViewSetCreate(TestCase):
|
||||
def setUp(self):
|
||||
self.request = MagicMock()
|
||||
self.request.data.get.return_value = None
|
||||
self.request.user.is_authenticated.return_value = False
|
||||
self.view_instance = MotionViewSet()
|
||||
self.view_instance.request = self.request
|
||||
self.view_instance.format_kwarg = MagicMock()
|
||||
@ -21,7 +22,6 @@ class MotionViewSetCreate(TestCase):
|
||||
@patch('openslides.motions.views.has_perm')
|
||||
@patch('openslides.motions.views.config')
|
||||
def test_simple_create(self, mock_config, mock_has_perm, mock_icd):
|
||||
self.request.user = 1
|
||||
mock_has_perm.return_value = True
|
||||
|
||||
self.view_instance.create(self.request)
|
||||
|
Loading…
Reference in New Issue
Block a user