Merge pull request #2283 from normanjaeckel/MotionComments

Added generic fields for comments for motions.
This commit is contained in:
Norman Jäckel 2016-09-07 22:11:59 +02:00 committed by GitHub
commit 41cb8b37cd
16 changed files with 378 additions and 36 deletions

View File

@ -24,6 +24,7 @@ Motions:
- Added origin field. - Added origin field.
- Added button to sort and number all motions in a category. - Added button to sort and number all motions in a category.
- Introduced pdfMake for clientside generation of PDFs. - Introduced pdfMake for clientside generation of PDFs.
- Added configurable fields for comments.
Users: Users:
- Added field is_committee and new default group Committees. - Added field is_committee and new default group Committees.

View File

@ -171,6 +171,8 @@ OpenSlides uses the following projects or parts of them:
* `django-jsonfield <https://github.com/bradjasper/django-jsonfield/>`_, * `django-jsonfield <https://github.com/bradjasper/django-jsonfield/>`_,
License: MIT License: MIT
* `jsonschema <https://github.com/Julian/jsonschema>`_, License: MIT
* `natsort <https://pypi.python.org/pypi/natsort>`_, License: MIT * `natsort <https://pypi.python.org/pypi/natsort>`_, License: MIT
* `PyPDF2 <http://mstamy2.github.io/PyPDF2/>`_, License: BSD * `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 <http://www.js-data.io>`_, License: MIT
* `js-data-angular <http://www.js-data.io/docs/js-data-angular>`_, 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 * `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 * `lodash <https://lodash.com/>`_, License: MIT
* `ng-dialog <https://github.com/likeastore/ngDialog>`_, License: MIT * `ng-dialog <https://github.com/likeastore/ngDialog>`_, License: MIT
* `ng-file-upload <https://github.com/danialfarid/ng-file-upload>`_, License: MIT * `ng-file-upload <https://github.com/danialfarid/ng-file-upload>`_, License: MIT

View File

@ -25,6 +25,7 @@
"jquery.cookie": "~1.4.1", "jquery.cookie": "~1.4.1",
"js-data": "~2.8.2", "js-data": "~2.8.2",
"js-data-angular": "~3.1.0", "js-data-angular": "~3.1.0",
"jsen": "~0.6.1",
"lodash": "~3.10.0", "lodash": "~3.10.0",
"ng-dialog": "~0.5.6", "ng-dialog": "~0.5.6",
"ng-file-upload": "~11.2.3", "ng-file-upload": "~11.2.3",
@ -44,9 +45,9 @@
}, },
"tinymce-dist": { "tinymce-dist": {
"main": [ "main": [
"tinymce.js", "tinymce.js",
"themes/modern/theme.js", "themes/modern/theme.js",
"plugins/*/plugin.js" "plugins/*/plugin.js"
] ]
} }
}, },

View File

@ -26,7 +26,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
def get_restricted_data(self, full_data, user): def get_restricted_data(self, full_data, user):
""" """
Returns the restricted serialized data for the instance prepared 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. only get a result like the AssignmentShortSerializer would give them.
""" """
if user.has_perm('assignments.can_manage'): if user.has_perm('assignments.can_manage'):

View File

@ -1,3 +1,8 @@
import json
from jsonschema import ValidationError, validate
from ..core.config import config
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
@ -19,6 +24,87 @@ class MotionAccessPermissions(BaseAccessPermissions):
return MotionSerializer 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): class CategoryAccessPermissions(BaseAccessPermissions):
""" """

View File

@ -151,6 +151,20 @@ def get_config_variables():
group='Motions', group='Motions',
subgroup='Supporters') 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 # Voting and ballot papers
yield ConfigVariable( yield ConfigVariable(

View 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),
),
]

View File

@ -119,12 +119,18 @@ class Motion(RESTModelMixin, models.Model):
Users who support this motion. Users who support this motion.
""" """
comments = JSONField(null=True)
"""
Configurable fields for comments. Contains a list of strings.
"""
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_see', 'Can see motions'), ('can_see', 'Can see motions'),
('can_create', 'Can create motions'), ('can_create', 'Can create motions'),
('can_support', 'Can support motions'), ('can_support', 'Can support motions'),
('can_see_and_manage_comments', 'Can see and manage comments'),
('can_manage', 'Can manage motions'), ('can_manage', 'Can manage motions'),
) )
ordering = ('identifier', ) ordering = ('identifier', )

View File

@ -5,6 +5,7 @@ from openslides.poll.serializers import default_votes_validator
from openslides.utils.rest_api import ( from openslides.utils.rest_api import (
CharField, CharField,
DictField, DictField,
Field,
IntegerField, IntegerField,
ModelSerializer, ModelSerializer,
PrimaryKeyRelatedField, PrimaryKeyRelatedField,
@ -74,6 +75,28 @@ class WorkflowSerializer(ModelSerializer):
fields = ('id', 'name', 'states', 'first_state',) 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): class MotionLogSerializer(ModelSerializer):
""" """
Serializer for motion.models.MotionLog objects. Serializer for motion.models.MotionLog objects.
@ -209,6 +232,7 @@ class MotionSerializer(ModelSerializer):
Serializer for motion.models.Motion objects. Serializer for motion.models.Motion objects.
""" """
active_version = PrimaryKeyRelatedField(read_only=True) active_version = PrimaryKeyRelatedField(read_only=True)
comments = MotionCommentsJSONSerializerField(required=False)
log_messages = MotionLogSerializer(many=True, read_only=True) log_messages = MotionLogSerializer(many=True, read_only=True)
polls = MotionPollSerializer(many=True, read_only=True) polls = MotionPollSerializer(many=True, read_only=True)
reason = CharField(allow_blank=True, required=False, write_only=True) reason = CharField(allow_blank=True, required=False, write_only=True)
@ -236,6 +260,7 @@ class MotionSerializer(ModelSerializer):
'origin', 'origin',
'submitters', 'submitters',
'supporters', 'supporters',
'comments',
'state', 'state',
'workflow_id', 'workflow_id',
'tags', 'tags',
@ -257,6 +282,7 @@ class MotionSerializer(ModelSerializer):
motion.identifier = validated_data.get('identifier') motion.identifier = validated_data.get('identifier')
motion.category = validated_data.get('category') motion.category = validated_data.get('category')
motion.origin = validated_data.get('origin', '') motion.origin = validated_data.get('origin', '')
motion.comments = validated_data.get('comments')
motion.parent = validated_data.get('parent') motion.parent = validated_data.get('parent')
motion.reset_state(validated_data.get('workflow_id')) motion.reset_state(validated_data.get('workflow_id'))
motion.save() motion.save()
@ -274,8 +300,8 @@ class MotionSerializer(ModelSerializer):
""" """
Customized method to update a motion. Customized method to update a motion.
""" """
# Identifier, category and origin. # Identifier, category, origin and comments.
for key in ('identifier', 'category', 'origin'): for key in ('identifier', 'category', 'origin', 'comments'):
if key in validated_data.keys(): if key in validated_data.keys():
setattr(motion, key, validated_data[key]) setattr(motion, key, validated_data[key])

View File

@ -117,18 +117,23 @@ angular.module('OpenSlidesApp.motions', [
.factory('Motion', [ .factory('Motion', [
'DS', 'DS',
'MotionPoll', 'MotionPoll',
'MotionComment',
'jsDataModel', 'jsDataModel',
'gettext', 'gettext',
'operator', 'operator',
'Config', 'Config',
'lineNumberingService', 'lineNumberingService',
function(DS, MotionPoll, jsDataModel, gettext, operator, Config, lineNumberingService) { function(DS, MotionPoll, MotionComment, jsDataModel, gettext, operator, Config, lineNumberingService) {
var name = 'motions/motion'; var name = 'motions/motion';
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
verboseName: gettext('Motion'), verboseName: gettext('Motion'),
verboseNamePlural: gettext('Motions'), verboseNamePlural: gettext('Motions'),
validate: function (resource, data, callback) {
MotionComment.populateFieldsReverse(data);
callback(null, data);
},
methods: { methods: {
getResourceName: function () { getResourceName: function () {
return name; return name;

View File

@ -208,7 +208,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
}; };
}) })
.factory('PollContentProvider', function() { .factory('PollContentProvider', function() {
/** /**
* Generates a content provider for polls * Generates a content provider for polls
* @constructor * @constructor
@ -216,7 +215,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
* @param {string} id - if of poll * @param {string} id - if of poll
* @param {object} gettextCatalog - for translation * @param {object} gettextCatalog - for translation
*/ */
var createInstance = function(title, id, gettextCatalog){ var createInstance = function(title, id, gettextCatalog){
//left and top margin for a single sheet //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) // Service for generic motion form (create and update)
.factory('MotionForm', [ .factory('MotionForm', [
'gettextCatalog', 'gettextCatalog',
'operator', 'operator',
'Editor', 'Editor',
'MotionComment',
'Category', 'Category',
'Config', 'Config',
'Mediafile', 'Mediafile',
@ -540,7 +651,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'Workflow', 'Workflow',
'Agenda', 'Agenda',
'AgendaTree', '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 { return {
// ngDialog for motion form // ngDialog for motion form
getDialog: function (motion) { getDialog: function (motion) {
@ -548,6 +659,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
if (motion) { if (motion) {
resolve = { resolve = {
motion: function() { motion: function() {
MotionComment.populateFields(motion);
return motion; return motion;
}, },
agenda_item: function(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 ...') placeholder: gettextCatalog.getString('Select or search a supporter ...')
}, },
hideExpression: '!model.more' hideExpression: '!model.more'
}, }]
{ .concat(MotionComment.getFormFields())
.concat([{
key: 'workflow_id', key: 'workflow_id',
type: 'select-single', type: 'select-single',
templateOptions: { templateOptions: {
@ -720,7 +833,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
placeholder: gettextCatalog.getString('Select or search a workflow ...') placeholder: gettextCatalog.getString('Select or search a workflow ...')
}, },
hideExpression: '!model.more', hideExpression: '!model.more',
}]; }]);
} }
}; };
} }
@ -936,6 +1049,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'$http', '$http',
'$timeout', '$timeout',
'ngDialog', 'ngDialog',
'MotionComment',
'MotionForm', 'MotionForm',
'Motion', 'Motion',
'Category', 'Category',
@ -953,9 +1067,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'PdfMakeDocumentProvider', 'PdfMakeDocumentProvider',
'gettextCatalog', 'gettextCatalog',
'diffService', 'diffService',
function($scope, $http, $timeout, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Editor, function($scope, $http, $timeout, ngDialog, MotionComment, MotionForm, Motion, Category, Mediafile, Tag,
Config, motion, SingleMotionContentProvider, MotionContentProvider, PollContentProvider, PdfMakeConverter, User, Workflow, Editor, Config, motion, SingleMotionContentProvider, MotionContentProvider,
PdfMakeDocumentProvider, gettextCatalog, diffService) { PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, gettextCatalog, diffService) {
Motion.bindOne(motion.id, $scope, 'motion'); Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
@ -965,6 +1079,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
Motion.loadRelations(motion, 'agenda_item'); Motion.loadRelations(motion, 'agenda_item');
$scope.version = motion.active_version; $scope.version = motion.active_version;
$scope.isCollapsed = true; $scope.isCollapsed = true;
$scope.commentsFields = MotionComment.getFields();
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value; $scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
$scope.lineBrokenText = motion.getTextWithLineBreaks($scope.version); $scope.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
if (motion.parent_id) { if (motion.parent_id) {

View File

@ -369,3 +369,12 @@
</div> </div>
</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>

View File

@ -1,4 +1,4 @@
from django.db import transaction from django.db import IntegrityError, transaction
from django.http import Http404 from django.http import Http404
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -67,6 +67,27 @@ class MotionViewSet(ModelViewSet):
result = False result = False
return result 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): def create(self, request, *args, **kwargs):
""" """
Customized view endpoint to create a new motion. 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. # Non-staff users are not allowed to send submitter or supporter data.
self.permission_denied(request) 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. # Validate data and create motion.
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -114,6 +141,12 @@ class MotionViewSet(ModelViewSet):
if key not in whitelist: if key not in whitelist:
# Non-staff users are allowed to send only some data. Ignore other data. # Non-staff users are allowed to send only some data. Ignore other data.
del request.data[key] 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. # Validate data and update motion.
serializer = self.get_serializer( serializer = self.get_serializer(
@ -327,24 +360,30 @@ class CategoryViewSet(ModelViewSet):
motion_dict[motion.pk] = motion motion_dict[motion.pk] = motion
motions = [motion_dict[pk] for pk in motion_list] motions = [motion_dict[pk] for pk in motion_list]
with transaction.atomic(): try:
for motion in motions: with transaction.atomic():
motion.identifier = None for motion in motions:
motion.save() motion.identifier = None
motion.save()
for motion in motions: for motion in motions:
if motion.is_amendment(): if motion.is_amendment():
parent_identifier = motion.parent.identifier or '' parent_identifier = motion.parent.identifier or ''
prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix']) prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix'])
number += 1 number += 1
identifier = '%s%d' % (prefix, number) identifier = '%s%d' % (prefix, number)
motion.identifier = identifier motion.identifier = identifier
motion.identifier_number = number motion.identifier_number = number
motion.save() motion.save()
except IntegrityError:
message = _('All motions in category {category} numbered ' message = _('Error: At least one identifier of this category does '
'successfully.').format(category=category) 'already exist in another category.')
return Response({'detail': message}) 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): class WorkflowViewSet(ModelViewSet):

View File

@ -36,6 +36,7 @@ def create_builtin_groups_and_admin(**kwargs):
'motions.can_create', 'motions.can_create',
'motions.can_manage', 'motions.can_manage',
'motions.can_see', 'motions.can_see',
'motions.can_see_and_manage_comments',
'motions.can_support', 'motions.can_support',
'users.can_manage', 'users.can_manage',
'users.can_see_extra_data', '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_see'],
permission_dict['motions.can_create'], permission_dict['motions.can_create'],
permission_dict['motions.can_manage'], permission_dict['motions.can_manage'],
permission_dict['motions.can_see_and_manage_comments'],
permission_dict['users.can_see_name'], permission_dict['users.can_see_name'],
permission_dict['users.can_manage'], permission_dict['users.can_manage'],
permission_dict['users.can_see_extra_data'],) permission_dict['users.can_see_extra_data'],)

View File

@ -5,6 +5,7 @@ channels>=0.15,<1.0
djangorestframework>=3.4,<3.5 djangorestframework>=3.4,<3.5
html5lib>=0.99,<1.0 html5lib>=0.99,<1.0
jsonfield>=0.9.19,<1.1 jsonfield>=0.9.19,<1.1
jsonschema>=2.5,<2.6
natsort>=3.2,<5.1 natsort>=3.2,<5.1
PyPDF2>=1.25.0,<1.27 PyPDF2>=1.25.0,<1.27
reportlab>=3.0,<3.4 reportlab>=3.0,<3.4

View File

@ -29,9 +29,8 @@ class CreateMotion(TestCase):
reverse('motion-list'), reverse('motion-list'),
{'title': 'test_title_OoCoo3MeiT9li5Iengu9', {'title': 'test_title_OoCoo3MeiT9li5Iengu9',
'text': 'test_text_thuoz0iecheiheereiCi'}) 'text': 'test_text_thuoz0iecheiheereiCi'})
motion = Motion.objects.get()
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
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())