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/>`_,
|
* `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
|
||||||
|
@ -25,7 +25,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",
|
||||||
|
@ -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})
|
||||||
|
|
||||||
|
@ -776,6 +776,11 @@ img {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Config **/
|
||||||
|
.input-comments > div {
|
||||||
|
margin-bottom: 5px
|
||||||
|
}
|
||||||
|
|
||||||
/** Footer **/
|
/** Footer **/
|
||||||
#footer {
|
#footer {
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -861,6 +861,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];
|
||||||
}
|
}
|
||||||
@ -1015,7 +1016,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;
|
||||||
|
|
||||||
@ -1024,6 +1026,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);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -166,12 +166,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')
|
||||||
|
@ -310,79 +310,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',
|
||||||
@ -397,7 +329,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 = [];
|
||||||
}
|
}
|
||||||
@ -407,7 +339,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] || '');
|
||||||
|
@ -1052,7 +1052,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
|||||||
'$scope',
|
'$scope',
|
||||||
'$http',
|
'$http',
|
||||||
'ngDialog',
|
'ngDialog',
|
||||||
'MotionComment',
|
|
||||||
'MotionForm',
|
'MotionForm',
|
||||||
'Motion',
|
'Motion',
|
||||||
'Category',
|
'Category',
|
||||||
@ -1070,9 +1069,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');
|
||||||
@ -1082,7 +1081,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');
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user