New ui element for comments in config (closes #2348)
This commit is contained in:
parent
53ac7c2348
commit
d13e11beb1
@ -171,8 +171,6 @@ 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
|
||||
@ -218,7 +216,6 @@ 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,7 +25,6 @@
|
||||
"jquery.cookie": "~1.4.1",
|
||||
"js-data": "~2.9.0",
|
||||
"js-data-angular": "~3.2.1",
|
||||
"jsen": "~0.6.1",
|
||||
"lodash": "~4.16.0",
|
||||
"ng-dialog": "~0.6.4",
|
||||
"ng-file-upload": "~11.2.3",
|
||||
|
@ -12,6 +12,7 @@ INPUT_TYPE_MAPPING = {
|
||||
'boolean': bool,
|
||||
'choice': str,
|
||||
'colorpicker': str,
|
||||
'comments': list,
|
||||
'resolution': dict}
|
||||
|
||||
|
||||
@ -98,6 +99,19 @@ class ConfigHandler:
|
||||
value['height'] < 600 or value['height'] > 2160):
|
||||
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.
|
||||
ConfigStore.objects.update_or_create(key=key, defaults={'value': value})
|
||||
|
||||
|
@ -776,6 +776,11 @@ img {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/** Config **/
|
||||
.input-comments > div {
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
/** Footer **/
|
||||
#footer {
|
||||
float: left;
|
||||
|
@ -861,6 +861,7 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
boolean: 'checkbox',
|
||||
choice: 'choice',
|
||||
colorpicker: 'colorpicker',
|
||||
comments: 'comments',
|
||||
resolution: 'resolution',
|
||||
}[type];
|
||||
}
|
||||
@ -1015,7 +1016,8 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
'$scope',
|
||||
'Config',
|
||||
'configOptions',
|
||||
function($scope, Config, configOptions) {
|
||||
'gettextCatalog',
|
||||
function($scope, Config, configOptions, gettextCatalog) {
|
||||
Config.bindAll({}, $scope, 'configs');
|
||||
$scope.configGroups = configOptions.data.config_groups;
|
||||
|
||||
@ -1024,6 +1026,19 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
Config.get(key).value = value;
|
||||
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);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
@ -31,6 +31,37 @@
|
||||
type="number">
|
||||
</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 -->
|
||||
<input ng-if="type == 'colorpicker'"
|
||||
colorpicker
|
||||
|
@ -1,7 +1,3 @@
|
||||
import json
|
||||
|
||||
from jsonschema import ValidationError, validate
|
||||
|
||||
from ..core.config import config
|
||||
from ..utils.access_permissions import BaseAccessPermissions
|
||||
|
||||
@ -34,7 +30,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||
data = full_data
|
||||
else:
|
||||
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'):
|
||||
try:
|
||||
data['comments'][i] = None
|
||||
@ -58,68 +54,6 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||
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):
|
||||
"""
|
||||
|
@ -166,12 +166,9 @@ def get_config_variables():
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_comments',
|
||||
default_value='Comment',
|
||||
input_type='text',
|
||||
default_value=[{'name': 'Comment', 'public': True}],
|
||||
input_type='comments',
|
||||
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')
|
||||
|
@ -310,79 +310,11 @@ angular.module('OpenSlidesApp.motions', [
|
||||
'operator',
|
||||
function (Config, operator) {
|
||||
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();
|
||||
var fields = Config.get('motions_comments').value;
|
||||
return _.map(
|
||||
fields,
|
||||
function (field) {
|
||||
// TODO: Hide non-public fields for unauthorized users.
|
||||
return {
|
||||
key: 'comment ' + field.name,
|
||||
type: 'input',
|
||||
@ -397,7 +329,7 @@ angular.module('OpenSlidesApp.motions', [
|
||||
populateFields: function (motion) {
|
||||
// Populate content of motion.comments to the single comment
|
||||
// fields like motion['comment MyComment'], motion['comment MyOtherComment'], ...
|
||||
var fields = this.getFields();
|
||||
var fields = Config.get('motions_comments').value;
|
||||
if (!motion.comments) {
|
||||
motion.comments = [];
|
||||
}
|
||||
@ -407,7 +339,7 @@ angular.module('OpenSlidesApp.motions', [
|
||||
},
|
||||
populateFieldsReverse: function (motion) {
|
||||
// Reverse equivalent to populateFields.
|
||||
var fields = this.getFields();
|
||||
var fields = Config.get('motions_comments').value;
|
||||
motion.comments = [];
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
motion.comments.push(motion['comment ' + fields[i].name] || '');
|
||||
|
@ -1052,7 +1052,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
'$scope',
|
||||
'$http',
|
||||
'ngDialog',
|
||||
'MotionComment',
|
||||
'MotionForm',
|
||||
'Motion',
|
||||
'Category',
|
||||
@ -1070,9 +1069,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
'MotionInlineEditing',
|
||||
'gettextCatalog',
|
||||
'Projector',
|
||||
function($scope, $http, ngDialog, MotionComment, MotionForm, Motion, Category, Mediafile, Tag,
|
||||
User, Workflow, Config, motion, SingleMotionContentProvider, MotionContentProvider,
|
||||
PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog, Projector) {
|
||||
function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Config,
|
||||
motion, SingleMotionContentProvider, MotionContentProvider, PollContentProvider,
|
||||
PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog, Projector) {
|
||||
Motion.bindOne(motion.id, $scope, 'motion');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||
@ -1082,7 +1081,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.commentsFields = Config.get('motions_comments').value;
|
||||
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
|
||||
if (motion.parent_id) {
|
||||
Motion.bindOne(motion.parent_id, $scope, 'parent');
|
||||
|
@ -5,7 +5,6 @@ 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
|
||||
|
@ -16,6 +16,7 @@ class CreateMotion(TestCase):
|
||||
Tests motion creation.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.client.login(username='admin', password='admin')
|
||||
|
||||
def test_simple(self):
|
||||
@ -106,6 +107,21 @@ class CreateMotion(TestCase):
|
||||
motion = Motion.objects.get()
|
||||
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):
|
||||
"""
|
||||
Test to create a motion with a specific workflow.
|
||||
|
Loading…
Reference in New Issue
Block a user