Merge pull request #2380 from FinnStutzenstein/Issue2348

New ui element for comments in config (closes #2348)
This commit is contained in:
Emanuel Schütze 2016-09-24 13:34:08 +02:00 committed by GitHub
commit 50f5d0a33a
12 changed files with 92 additions and 154 deletions

View File

@ -171,8 +171,6 @@ 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
@ -218,7 +216,6 @@ 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

@ -27,7 +27,6 @@
"jquery.cookie": "~1.4.1", "jquery.cookie": "~1.4.1",
"js-data": "~2.9.0", "js-data": "~2.9.0",
"js-data-angular": "~3.2.1", "js-data-angular": "~3.2.1",
"jsen": "~0.6.1",
"lodash": "~4.16.0", "lodash": "~4.16.0",
"ng-dialog": "~0.6.4", "ng-dialog": "~0.6.4",
"ng-file-upload": "~11.2.3", "ng-file-upload": "~11.2.3",

View File

@ -12,6 +12,7 @@ INPUT_TYPE_MAPPING = {
'boolean': bool, 'boolean': bool,
'choice': str, 'choice': str,
'colorpicker': str, 'colorpicker': str,
'comments': list,
'resolution': dict} 'resolution': dict}
@ -98,6 +99,19 @@ class ConfigHandler:
value['height'] < 600 or value['height'] > 2160): value['height'] < 600 or value['height'] > 2160):
raise ConfigError(_('The Resolution have to be between 800x600 and 3840x2160.')) raise ConfigError(_('The Resolution have to be between 800x600 and 3840x2160.'))
if config_variable.input_type == 'comments':
if not isinstance(value, list):
raise ConfigError(_('motions_comments has to be a list.'))
for comment in value:
if not isinstance(comment, dict):
raise ConfigError(_('Each element in motions_comments has to be a dict.'))
if comment.get('name') is None or comment.get('public') is None:
raise ConfigError(_('A name and a public property have to be given.'))
if not isinstance(comment['name'], str):
raise ConfigError(_('name has to be string.'))
if not isinstance(comment['public'], bool):
raise ConfigError(_('public property has to be bool.'))
# Save the new value to the database. # Save the new value to the database.
ConfigStore.objects.update_or_create(key=key, defaults={'value': value}) ConfigStore.objects.update_or_create(key=key, defaults={'value': value})

View File

@ -777,6 +777,11 @@ img {
background-color: transparent; background-color: transparent;
} }
/** Config **/
.input-comments > div {
margin-bottom: 5px
}
/** Footer **/ /** Footer **/
#footer { #footer {
float: left; float: left;

View File

@ -862,6 +862,7 @@ angular.module('OpenSlidesApp.core.site', [
boolean: 'checkbox', boolean: 'checkbox',
choice: 'choice', choice: 'choice',
colorpicker: 'colorpicker', colorpicker: 'colorpicker',
comments: 'comments',
resolution: 'resolution', resolution: 'resolution',
}[type]; }[type];
} }
@ -1016,7 +1017,8 @@ angular.module('OpenSlidesApp.core.site', [
'$scope', '$scope',
'Config', 'Config',
'configOptions', 'configOptions',
function($scope, Config, configOptions) { 'gettextCatalog',
function($scope, Config, configOptions, gettextCatalog) {
Config.bindAll({}, $scope, 'configs'); Config.bindAll({}, $scope, 'configs');
$scope.configGroups = configOptions.data.config_groups; $scope.configGroups = configOptions.data.config_groups;
@ -1025,6 +1027,19 @@ angular.module('OpenSlidesApp.core.site', [
Config.get(key).value = value; Config.get(key).value = value;
Config.save(key); Config.save(key);
}; };
/* For comments input */
$scope.addComment = function (key, parent) {
parent.value.push({
name: gettextCatalog.getString('New'),
public: false,
});
$scope.save(key, parent.value);
};
$scope.removeComment = function (key, parent, index) {
parent.value.splice(index, 1);
$scope.save(key, parent.value);
};
} }
]) ])

View File

@ -31,6 +31,37 @@
type="number"> type="number">
</span> </span>
<!-- comments -->
<div class="input-comments" ng-if="type == 'comments'">
<div ng-repeat="entry in $parent.value" class="input-group">
<input ng-model="entry.name"
ng-model-options="{debounce: 1000}"
ng-change="save(configOption.key, $parent.value)"
class="form-control"
id="{{ key }}"
type="text">
<span class="input-group-btn">
<button type=button" class="btn btn-default"
ng-click="entry.public = !entry.public; save(configOption.key, $parent.value);">
<i class="fa" ng-class="entry.public ? 'fa-unlock' : 'fa-lock'"></i>
{{ (entry.public ? 'Public' : 'Private') | translate }}
</button>
<button type="button" class="btn btn-default"
ng-click="removeComment(configOption.key, $parent, $index)">
<i class="fa fa-minus"></i>
<translate>Remove</translate>
</button>
</span>
</div>
<div>
<button type="button" ng-click="addComment(configOption.key, $parent)"
class="btn btn-default btn-sm">
<i class="fa fa-plus"></i>
<translate>Add comment</translate>
</button>
</div>
</div>
<!-- colorpicker --> <!-- colorpicker -->
<input ng-if="type == 'colorpicker'" <input ng-if="type == 'colorpicker'"
colorpicker colorpicker

View File

@ -1,7 +1,3 @@
import json
from jsonschema import ValidationError, validate
from ..core.config import config from ..core.config import config
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
@ -34,7 +30,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
data = full_data data = full_data
else: else:
data = full_data.copy() data = full_data.copy()
for i, field in enumerate(self.get_comments_config_fields()): for i, field in enumerate(config['motions_comments']):
if not field.get('public'): if not field.get('public'):
try: try:
data['comments'][i] = None data['comments'][i] = None
@ -58,68 +54,6 @@ class MotionAccessPermissions(BaseAccessPermissions):
pass pass
return data 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

@ -161,12 +161,9 @@ def get_config_variables():
yield ConfigVariable( yield ConfigVariable(
name='motions_comments', name='motions_comments',
default_value='Comment', default_value=[{'name': 'Comment', 'public': True}],
input_type='text', input_type='comments',
label='Comment fields for motions', 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, weight=353,
group='Motions', group='Motions',
subgroup='Comments') subgroup='Comments')

View File

@ -329,79 +329,11 @@ angular.module('OpenSlidesApp.motions', [
'operator', 'operator',
function (Config, operator) { function (Config, operator) {
return { 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 () { getFormFields: function () {
var fields = this.getFields(); var fields = Config.get('motions_comments').value;
return _.map( return _.map(
fields, fields,
function (field) { function (field) {
// TODO: Hide non-public fields for unauthorized users.
return { return {
key: 'comment ' + field.name, key: 'comment ' + field.name,
type: 'input', type: 'input',
@ -416,7 +348,7 @@ angular.module('OpenSlidesApp.motions', [
populateFields: function (motion) { populateFields: function (motion) {
// Populate content of motion.comments to the single comment // Populate content of motion.comments to the single comment
// fields like motion['comment MyComment'], motion['comment MyOtherComment'], ... // fields like motion['comment MyComment'], motion['comment MyOtherComment'], ...
var fields = this.getFields(); var fields = Config.get('motions_comments').value;
if (!motion.comments) { if (!motion.comments) {
motion.comments = []; motion.comments = [];
} }
@ -426,7 +358,7 @@ angular.module('OpenSlidesApp.motions', [
}, },
populateFieldsReverse: function (motion) { populateFieldsReverse: function (motion) {
// Reverse equivalent to populateFields. // Reverse equivalent to populateFields.
var fields = this.getFields(); var fields = Config.get('motions_comments').value;
motion.comments = []; motion.comments = [];
for (var i = 0; i < fields.length; i++) { for (var i = 0; i < fields.length; i++) {
motion.comments.push(motion['comment ' + fields[i].name] || ''); motion.comments.push(motion['comment ' + fields[i].name] || '');

View File

@ -1056,7 +1056,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'$scope', '$scope',
'$http', '$http',
'ngDialog', 'ngDialog',
'MotionComment',
'MotionForm', 'MotionForm',
'Motion', 'Motion',
'Category', 'Category',
@ -1074,9 +1073,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'MotionInlineEditing', 'MotionInlineEditing',
'gettextCatalog', 'gettextCatalog',
'Projector', 'Projector',
function($scope, $http, ngDialog, MotionComment, MotionForm, Motion, Category, Mediafile, Tag, function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Config,
User, Workflow, Config, motion, SingleMotionContentProvider, MotionContentProvider, motion, SingleMotionContentProvider, MotionContentProvider, PollContentProvider,
PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog, Projector) { PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog, Projector) {
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');
@ -1086,7 +1085,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.commentsFields = Config.get('motions_comments').value;
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value; $scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
if (motion.parent_id) { if (motion.parent_id) {
Motion.bindOne(motion.parent_id, $scope, 'parent'); Motion.bindOne(motion.parent_id, $scope, 'parent');

View File

@ -5,7 +5,6 @@ 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

@ -16,6 +16,7 @@ class CreateMotion(TestCase):
Tests motion creation. Tests motion creation.
""" """
def setUp(self): def setUp(self):
self.client = APIClient()
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
def test_simple(self): def test_simple(self):
@ -106,6 +107,21 @@ class CreateMotion(TestCase):
motion = Motion.objects.get() motion = Motion.objects.get()
self.assertEqual(motion.tags.get().name, 'test_tag_iRee3kiecoos4rorohth') self.assertEqual(motion.tags.get().name, 'test_tag_iRee3kiecoos4rorohth')
def test_with_multiple_comments(self):
config['motions_comments'] = [
{'name': 'comment1', 'public': True},
{'name': 'comment2', 'public': False}]
comments = ['comemnt1_sdpoiuffo3%7dwDwW&', 'comment2_iusd&D/TdskDWH&5DWas46WAd078']
response = self.client.post(
reverse('motion-list'),
{'title': 'title_test_sfdAaufd56HR7sd5FDq7av',
'text': 'text_test_fiuhefF86()ew1Ef346AF6W',
'comments': comments},
format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
self.assertEqual(motion.comments, comments)
def test_with_workflow(self): def test_with_workflow(self):
""" """
Test to create a motion with a specific workflow. Test to create a motion with a specific workflow.