Merge pull request #2283 from normanjaeckel/MotionComments
Added generic fields for comments for motions.
This commit is contained in:
commit
41cb8b37cd
@ -24,6 +24,7 @@ Motions:
|
||||
- Added origin field.
|
||||
- Added button to sort and number all motions in a category.
|
||||
- Introduced pdfMake for clientside generation of PDFs.
|
||||
- Added configurable fields for comments.
|
||||
|
||||
Users:
|
||||
- Added field is_committee and new default group Committees.
|
||||
|
@ -171,6 +171,8 @@ OpenSlides uses the following projects or parts of them:
|
||||
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield/>`_,
|
||||
License: MIT
|
||||
|
||||
* `jsonschema <https://github.com/Julian/jsonschema>`_, License: MIT
|
||||
|
||||
* `natsort <https://pypi.python.org/pypi/natsort>`_, License: MIT
|
||||
|
||||
* `PyPDF2 <http://mstamy2.github.io/PyPDF2/>`_, License: BSD
|
||||
@ -216,6 +218,7 @@ OpenSlides uses the following projects or parts of them:
|
||||
* `js-data <http://www.js-data.io>`_, License: MIT
|
||||
* `js-data-angular <http://www.js-data.io/docs/js-data-angular>`_, License: MIT
|
||||
* `js-data-http <http://www.js-data.io/docs/dshttpadapter>`_, License: MIT
|
||||
* `jsen <https://github.com/bugventure/jsen>`_, License: MIT
|
||||
* `lodash <https://lodash.com/>`_, License: MIT
|
||||
* `ng-dialog <https://github.com/likeastore/ngDialog>`_, License: MIT
|
||||
* `ng-file-upload <https://github.com/danialfarid/ng-file-upload>`_, License: MIT
|
||||
|
@ -25,6 +25,7 @@
|
||||
"jquery.cookie": "~1.4.1",
|
||||
"js-data": "~2.8.2",
|
||||
"js-data-angular": "~3.1.0",
|
||||
"jsen": "~0.6.1",
|
||||
"lodash": "~3.10.0",
|
||||
"ng-dialog": "~0.5.6",
|
||||
"ng-file-upload": "~11.2.3",
|
||||
@ -44,9 +45,9 @@
|
||||
},
|
||||
"tinymce-dist": {
|
||||
"main": [
|
||||
"tinymce.js",
|
||||
"themes/modern/theme.js",
|
||||
"plugins/*/plugin.js"
|
||||
"tinymce.js",
|
||||
"themes/modern/theme.js",
|
||||
"plugins/*/plugin.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -26,7 +26,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
||||
def get_restricted_data(self, full_data, user):
|
||||
"""
|
||||
Returns the restricted serialized data for the instance prepared
|
||||
for the user. Removes unpublushed polls for non admins so that they
|
||||
for the user. Removes unpublished polls for non admins so that they
|
||||
only get a result like the AssignmentShortSerializer would give them.
|
||||
"""
|
||||
if user.has_perm('assignments.can_manage'):
|
||||
|
@ -1,3 +1,8 @@
|
||||
import json
|
||||
|
||||
from jsonschema import ValidationError, validate
|
||||
|
||||
from ..core.config import config
|
||||
from ..utils.access_permissions import BaseAccessPermissions
|
||||
|
||||
|
||||
@ -19,6 +24,87 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||
|
||||
return MotionSerializer
|
||||
|
||||
def get_restricted_data(self, full_data, user):
|
||||
"""
|
||||
Returns the restricted serialized data for the instance prepared for
|
||||
the user. Removes non public comment fields for some unauthorized
|
||||
users.
|
||||
"""
|
||||
if user.has_perm('motions.can_see_and_manage_comments') or not full_data.get('comments'):
|
||||
data = full_data
|
||||
else:
|
||||
data = full_data.copy()
|
||||
for i, field in enumerate(self.get_comments_config_fields()):
|
||||
if not field.get('public'):
|
||||
try:
|
||||
data['comments'][i] = None
|
||||
except IndexError:
|
||||
# No data in range. Just do nothing.
|
||||
pass
|
||||
return data
|
||||
|
||||
def get_comments_config_fields(self):
|
||||
"""
|
||||
Take input from config field and parse it. It can be some
|
||||
JSON or just a comma separated list of strings.
|
||||
|
||||
The result is an array of objects. Each object contains
|
||||
at least the name of the comment field See configSchema.
|
||||
|
||||
Attention: This code does also exist on server side.
|
||||
"""
|
||||
configSchema = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Motion Comments",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"forRecommendation": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"forState": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": True
|
||||
}
|
||||
configValue = config['motions_comments']
|
||||
fields = None
|
||||
isJSON = True
|
||||
try:
|
||||
fields = json.loads(configValue)
|
||||
except ValueError:
|
||||
isJSON = False
|
||||
if isJSON:
|
||||
# Config is JSON. Validate it.
|
||||
try:
|
||||
validate(fields, configSchema)
|
||||
except ValidationError:
|
||||
fields = []
|
||||
else:
|
||||
# Config is a comma separated list of strings. Strip out
|
||||
# empty parts. All valid strings lead to public comment
|
||||
# fields.
|
||||
fields = map(
|
||||
lambda name: {'name': name, 'public': True},
|
||||
filter(
|
||||
lambda name: name,
|
||||
configValue.split(',')
|
||||
)
|
||||
)
|
||||
return fields
|
||||
|
||||
|
||||
class CategoryAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
|
@ -151,6 +151,20 @@ def get_config_variables():
|
||||
group='Motions',
|
||||
subgroup='Supporters')
|
||||
|
||||
# Comments
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_comments',
|
||||
default_value='Comment',
|
||||
input_type='text',
|
||||
label='Comment fields for motions',
|
||||
help_text='Use comma separated list of field names for public '
|
||||
'fields or use special JSON. Example: [{"name": "Hidden Comment", '
|
||||
'"public": false}, {"name": "Public Comment", "public": true}]',
|
||||
weight=353,
|
||||
group='Motions',
|
||||
subgroup='Comments')
|
||||
|
||||
# Voting and ballot papers
|
||||
|
||||
yield ConfigVariable(
|
||||
|
35
openslides/motions/migrations/0003_auto_20160819_0925.py
Normal file
35
openslides/motions/migrations/0003_auto_20160819_0925.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-08-19 09:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import jsonfield.fields
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0002_motion_origin'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='motion',
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'ordering': ('identifier',),
|
||||
'permissions': (
|
||||
('can_see', 'Can see motions'),
|
||||
('can_create', 'Can create motions'),
|
||||
('can_support', 'Can support motions'),
|
||||
('can_see_and_manage_comments', 'Can see and manage comments'),
|
||||
('can_manage', 'Can manage motions')),
|
||||
'verbose_name': 'Motion'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='comments',
|
||||
field=jsonfield.fields.JSONField(null=True),
|
||||
),
|
||||
]
|
@ -119,12 +119,18 @@ class Motion(RESTModelMixin, models.Model):
|
||||
Users who support this motion.
|
||||
"""
|
||||
|
||||
comments = JSONField(null=True)
|
||||
"""
|
||||
Configurable fields for comments. Contains a list of strings.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_see', 'Can see motions'),
|
||||
('can_create', 'Can create motions'),
|
||||
('can_support', 'Can support motions'),
|
||||
('can_see_and_manage_comments', 'Can see and manage comments'),
|
||||
('can_manage', 'Can manage motions'),
|
||||
)
|
||||
ordering = ('identifier', )
|
||||
|
@ -5,6 +5,7 @@ from openslides.poll.serializers import default_votes_validator
|
||||
from openslides.utils.rest_api import (
|
||||
CharField,
|
||||
DictField,
|
||||
Field,
|
||||
IntegerField,
|
||||
ModelSerializer,
|
||||
PrimaryKeyRelatedField,
|
||||
@ -74,6 +75,28 @@ class WorkflowSerializer(ModelSerializer):
|
||||
fields = ('id', 'name', 'states', 'first_state',)
|
||||
|
||||
|
||||
class MotionCommentsJSONSerializerField(Field):
|
||||
"""
|
||||
Serializer for motions's comments JSONField.
|
||||
"""
|
||||
def to_representation(self, obj):
|
||||
"""
|
||||
Returns the value of the field.
|
||||
"""
|
||||
return obj
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
Checks that data is a list of strings.
|
||||
"""
|
||||
if type(data) is not list:
|
||||
raise ValidationError({'detail': 'Data must be an array.'})
|
||||
for element in data:
|
||||
if type(element) is not str:
|
||||
raise ValidationError({'detail': 'Data must be an array of strings.'})
|
||||
return data
|
||||
|
||||
|
||||
class MotionLogSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionLog objects.
|
||||
@ -209,6 +232,7 @@ class MotionSerializer(ModelSerializer):
|
||||
Serializer for motion.models.Motion objects.
|
||||
"""
|
||||
active_version = PrimaryKeyRelatedField(read_only=True)
|
||||
comments = MotionCommentsJSONSerializerField(required=False)
|
||||
log_messages = MotionLogSerializer(many=True, read_only=True)
|
||||
polls = MotionPollSerializer(many=True, read_only=True)
|
||||
reason = CharField(allow_blank=True, required=False, write_only=True)
|
||||
@ -236,6 +260,7 @@ class MotionSerializer(ModelSerializer):
|
||||
'origin',
|
||||
'submitters',
|
||||
'supporters',
|
||||
'comments',
|
||||
'state',
|
||||
'workflow_id',
|
||||
'tags',
|
||||
@ -257,6 +282,7 @@ class MotionSerializer(ModelSerializer):
|
||||
motion.identifier = validated_data.get('identifier')
|
||||
motion.category = validated_data.get('category')
|
||||
motion.origin = validated_data.get('origin', '')
|
||||
motion.comments = validated_data.get('comments')
|
||||
motion.parent = validated_data.get('parent')
|
||||
motion.reset_state(validated_data.get('workflow_id'))
|
||||
motion.save()
|
||||
@ -274,8 +300,8 @@ class MotionSerializer(ModelSerializer):
|
||||
"""
|
||||
Customized method to update a motion.
|
||||
"""
|
||||
# Identifier, category and origin.
|
||||
for key in ('identifier', 'category', 'origin'):
|
||||
# Identifier, category, origin and comments.
|
||||
for key in ('identifier', 'category', 'origin', 'comments'):
|
||||
if key in validated_data.keys():
|
||||
setattr(motion, key, validated_data[key])
|
||||
|
||||
|
@ -117,18 +117,23 @@ angular.module('OpenSlidesApp.motions', [
|
||||
.factory('Motion', [
|
||||
'DS',
|
||||
'MotionPoll',
|
||||
'MotionComment',
|
||||
'jsDataModel',
|
||||
'gettext',
|
||||
'operator',
|
||||
'Config',
|
||||
'lineNumberingService',
|
||||
function(DS, MotionPoll, jsDataModel, gettext, operator, Config, lineNumberingService) {
|
||||
function(DS, MotionPoll, MotionComment, jsDataModel, gettext, operator, Config, lineNumberingService) {
|
||||
var name = 'motions/motion';
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
useClass: jsDataModel,
|
||||
verboseName: gettext('Motion'),
|
||||
verboseNamePlural: gettext('Motions'),
|
||||
validate: function (resource, data, callback) {
|
||||
MotionComment.populateFieldsReverse(data);
|
||||
callback(null, data);
|
||||
},
|
||||
methods: {
|
||||
getResourceName: function () {
|
||||
return name;
|
||||
|
@ -208,7 +208,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
};
|
||||
})
|
||||
.factory('PollContentProvider', function() {
|
||||
|
||||
/**
|
||||
* Generates a content provider for polls
|
||||
* @constructor
|
||||
@ -216,7 +215,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
* @param {string} id - if of poll
|
||||
* @param {object} gettextCatalog - for translation
|
||||
*/
|
||||
|
||||
var createInstance = function(title, id, gettextCatalog){
|
||||
|
||||
//left and top margin for a single sheet
|
||||
@ -527,11 +525,124 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
}
|
||||
])
|
||||
|
||||
// Service for generic comment fields
|
||||
.factory('MotionComment', [
|
||||
'Config',
|
||||
function (Config) {
|
||||
return {
|
||||
getFields: function () {
|
||||
// Take input from config field and parse it. It can be some
|
||||
// JSON or just a comma separated list of strings.
|
||||
//
|
||||
// The result is an array of objects. Each object contains
|
||||
// at least the name of the comment field See configSchema.
|
||||
//
|
||||
// Attention: This code does also exist on server side.
|
||||
var configSchema = {
|
||||
$schema: "http://json-schema.org/draft-04/schema#",
|
||||
title: "Motion Comments",
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
public: {
|
||||
type: "boolean"
|
||||
},
|
||||
forRecommendation: {
|
||||
type: "boolean"
|
||||
},
|
||||
forState: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
required: ["name"]
|
||||
},
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
};
|
||||
var configValue = Config.get('motions_comments').value;
|
||||
var fields;
|
||||
var isJSON = true;
|
||||
try {
|
||||
fields = JSON.parse(configValue);
|
||||
} catch (err) {
|
||||
isJSON = false;
|
||||
}
|
||||
if (isJSON) {
|
||||
// Config is JSON. Validate it.
|
||||
if (!jsen(configSchema)(fields)) {
|
||||
fields = [];
|
||||
}
|
||||
} else {
|
||||
// Config is a comma separated list of strings. Strip out
|
||||
// empty parts. All valid strings lead to public comment
|
||||
// fields.
|
||||
fields = _.map(
|
||||
_.filter(
|
||||
configValue.split(','),
|
||||
function (name) {
|
||||
return name;
|
||||
}),
|
||||
function (name) {
|
||||
return {
|
||||
'name': name,
|
||||
'public': true
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
return fields;
|
||||
},
|
||||
getFormFields: function () {
|
||||
var fields = this.getFields();
|
||||
return _.map(
|
||||
fields,
|
||||
function (field) {
|
||||
// TODO: Hide non-public fields for unauthorized users.
|
||||
return {
|
||||
key: 'comment ' + field.name,
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: field.name,
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
populateFields: function (motion) {
|
||||
// Populate content of motion.comments to the single comment
|
||||
// fields like motion['comment MyComment'], motion['comment MyOtherComment'], ...
|
||||
var fields = this.getFields();
|
||||
if (!motion.comments) {
|
||||
motion.comments = [];
|
||||
}
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
motion['comment ' + fields[i].name] = motion.comments[i];
|
||||
}
|
||||
},
|
||||
populateFieldsReverse: function (motion) {
|
||||
// Reverse equivalent to populateFields.
|
||||
var fields = this.getFields();
|
||||
motion.comments = [];
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
motion.comments.push(motion['comment ' + fields[i].name] || '');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// Service for generic motion form (create and update)
|
||||
.factory('MotionForm', [
|
||||
'gettextCatalog',
|
||||
'operator',
|
||||
'Editor',
|
||||
'MotionComment',
|
||||
'Category',
|
||||
'Config',
|
||||
'Mediafile',
|
||||
@ -540,7 +651,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
'Workflow',
|
||||
'Agenda',
|
||||
'AgendaTree',
|
||||
function (gettextCatalog, operator, Editor, Category, Config, Mediafile, Tag, User, Workflow, Agenda, AgendaTree) {
|
||||
function (gettextCatalog, operator, Editor, MotionComment, Category, Config, Mediafile, Tag, User, Workflow, Agenda, AgendaTree) {
|
||||
return {
|
||||
// ngDialog for motion form
|
||||
getDialog: function (motion) {
|
||||
@ -548,6 +659,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
if (motion) {
|
||||
resolve = {
|
||||
motion: function() {
|
||||
MotionComment.populateFields(motion);
|
||||
return motion;
|
||||
},
|
||||
agenda_item: function(Motion) {
|
||||
@ -708,8 +820,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
placeholder: gettextCatalog.getString('Select or search a supporter ...')
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
},
|
||||
{
|
||||
}]
|
||||
.concat(MotionComment.getFormFields())
|
||||
.concat([{
|
||||
key: 'workflow_id',
|
||||
type: 'select-single',
|
||||
templateOptions: {
|
||||
@ -720,7 +833,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
placeholder: gettextCatalog.getString('Select or search a workflow ...')
|
||||
},
|
||||
hideExpression: '!model.more',
|
||||
}];
|
||||
}]);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -936,6 +1049,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
'$http',
|
||||
'$timeout',
|
||||
'ngDialog',
|
||||
'MotionComment',
|
||||
'MotionForm',
|
||||
'Motion',
|
||||
'Category',
|
||||
@ -953,9 +1067,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
'PdfMakeDocumentProvider',
|
||||
'gettextCatalog',
|
||||
'diffService',
|
||||
function($scope, $http, $timeout, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Editor,
|
||||
Config, motion, SingleMotionContentProvider, MotionContentProvider, PollContentProvider, PdfMakeConverter,
|
||||
PdfMakeDocumentProvider, gettextCatalog, diffService) {
|
||||
function($scope, $http, $timeout, ngDialog, MotionComment, MotionForm, Motion, Category, Mediafile, Tag,
|
||||
User, Workflow, Editor, Config, motion, SingleMotionContentProvider, MotionContentProvider,
|
||||
PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, gettextCatalog, diffService) {
|
||||
Motion.bindOne(motion.id, $scope, 'motion');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||
@ -965,6 +1079,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
Motion.loadRelations(motion, 'agenda_item');
|
||||
$scope.version = motion.active_version;
|
||||
$scope.isCollapsed = true;
|
||||
$scope.commentsFields = MotionComment.getFields();
|
||||
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
|
||||
$scope.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
|
||||
if (motion.parent_id) {
|
||||
|
@ -369,3 +369,12 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<h3>Motion Comments</h3>
|
||||
<div ng-repeat="field in commentsFields">
|
||||
<p ng-if="field.public || operator.hasPerms('motions.can_see_and_manage_comments')">
|
||||
{{ field.name }}: {{ motion.comments[$index] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.db import transaction
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.http import Http404
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import ugettext as _
|
||||
@ -67,6 +67,27 @@ class MotionViewSet(ModelViewSet):
|
||||
result = False
|
||||
return result
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
Customized view endpoint to list all motions.
|
||||
|
||||
Hides non public comment fields for some users.
|
||||
"""
|
||||
response = super().list(request, *args, **kwargs)
|
||||
for i, motion in enumerate(response.data):
|
||||
response.data[i] = self.get_access_permissions().get_restricted_data(motion, self.request.user)
|
||||
return response
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""
|
||||
Customized view endpoint to retrieve a motion.
|
||||
|
||||
Hides non public comment fields for some users.
|
||||
"""
|
||||
response = super().retrieve(request, *args, **kwargs)
|
||||
response.data = self.get_access_permissions().get_restricted_data(response.data, self.request.user)
|
||||
return response
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Customized view endpoint to create a new motion.
|
||||
@ -77,6 +98,12 @@ class MotionViewSet(ModelViewSet):
|
||||
# Non-staff users are not allowed to send submitter or supporter data.
|
||||
self.permission_denied(request)
|
||||
|
||||
# Check permission to send comment data.
|
||||
if (not request.user.has_perm('motions.can_see_and_manage_comments') and
|
||||
request.data.get('comments')):
|
||||
# Some users are not allowed to send comments data.
|
||||
self.permission_denied(request)
|
||||
|
||||
# Validate data and create motion.
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
@ -114,6 +141,12 @@ class MotionViewSet(ModelViewSet):
|
||||
if key not in whitelist:
|
||||
# Non-staff users are allowed to send only some data. Ignore other data.
|
||||
del request.data[key]
|
||||
if not request.user.has_perm('motions.can_see_and_manage_comments'):
|
||||
try:
|
||||
del request.data['comments']
|
||||
except KeyError:
|
||||
# No comments here. Just do nothing.
|
||||
pass
|
||||
|
||||
# Validate data and update motion.
|
||||
serializer = self.get_serializer(
|
||||
@ -327,24 +360,30 @@ class CategoryViewSet(ModelViewSet):
|
||||
motion_dict[motion.pk] = motion
|
||||
motions = [motion_dict[pk] for pk in motion_list]
|
||||
|
||||
with transaction.atomic():
|
||||
for motion in motions:
|
||||
motion.identifier = None
|
||||
motion.save()
|
||||
try:
|
||||
with transaction.atomic():
|
||||
for motion in motions:
|
||||
motion.identifier = None
|
||||
motion.save()
|
||||
|
||||
for motion in motions:
|
||||
if motion.is_amendment():
|
||||
parent_identifier = motion.parent.identifier or ''
|
||||
prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix'])
|
||||
number += 1
|
||||
identifier = '%s%d' % (prefix, number)
|
||||
motion.identifier = identifier
|
||||
motion.identifier_number = number
|
||||
motion.save()
|
||||
|
||||
message = _('All motions in category {category} numbered '
|
||||
'successfully.').format(category=category)
|
||||
return Response({'detail': message})
|
||||
for motion in motions:
|
||||
if motion.is_amendment():
|
||||
parent_identifier = motion.parent.identifier or ''
|
||||
prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix'])
|
||||
number += 1
|
||||
identifier = '%s%d' % (prefix, number)
|
||||
motion.identifier = identifier
|
||||
motion.identifier_number = number
|
||||
motion.save()
|
||||
except IntegrityError:
|
||||
message = _('Error: At least one identifier of this category does '
|
||||
'already exist in another category.')
|
||||
response = Response({'detail': message}, status_code=400)
|
||||
else:
|
||||
message = _('All motions in category {category} numbered '
|
||||
'successfully.').format(category=category)
|
||||
response = Response({'detail': message})
|
||||
return response
|
||||
|
||||
|
||||
class WorkflowViewSet(ModelViewSet):
|
||||
|
@ -36,6 +36,7 @@ def create_builtin_groups_and_admin(**kwargs):
|
||||
'motions.can_create',
|
||||
'motions.can_manage',
|
||||
'motions.can_see',
|
||||
'motions.can_see_and_manage_comments',
|
||||
'motions.can_support',
|
||||
'users.can_manage',
|
||||
'users.can_see_extra_data',
|
||||
@ -106,6 +107,7 @@ def create_builtin_groups_and_admin(**kwargs):
|
||||
permission_dict['motions.can_see'],
|
||||
permission_dict['motions.can_create'],
|
||||
permission_dict['motions.can_manage'],
|
||||
permission_dict['motions.can_see_and_manage_comments'],
|
||||
permission_dict['users.can_see_name'],
|
||||
permission_dict['users.can_manage'],
|
||||
permission_dict['users.can_see_extra_data'],)
|
||||
|
@ -5,6 +5,7 @@ channels>=0.15,<1.0
|
||||
djangorestframework>=3.4,<3.5
|
||||
html5lib>=0.99,<1.0
|
||||
jsonfield>=0.9.19,<1.1
|
||||
jsonschema>=2.5,<2.6
|
||||
natsort>=3.2,<5.1
|
||||
PyPDF2>=1.25.0,<1.27
|
||||
reportlab>=3.0,<3.4
|
||||
|
@ -29,9 +29,8 @@ class CreateMotion(TestCase):
|
||||
reverse('motion-list'),
|
||||
{'title': 'test_title_OoCoo3MeiT9li5Iengu9',
|
||||
'text': 'test_text_thuoz0iecheiheereiCi'})
|
||||
|
||||
motion = Motion.objects.get()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
motion = Motion.objects.get()
|
||||
self.assertEqual(motion.title, 'test_title_OoCoo3MeiT9li5Iengu9')
|
||||
self.assertEqual(motion.identifier, '1')
|
||||
self.assertTrue(motion.submitters.exists())
|
||||
|
Loading…
Reference in New Issue
Block a user