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 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.
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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'):
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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(
|
||||||
|
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.
|
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', )
|
||||||
|
@ -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])
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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):
|
||||||
|
@ -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'],)
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
|
Loading…
Reference in New Issue
Block a user