Merge pull request #3376 from FinnStutzenstein/MotionCommentsRework

Rework on motion comments.
This commit is contained in:
Emanuel Schütze 2017-09-15 10:59:02 +02:00 committed by GitHub
commit 2c4a1d5a6c
16 changed files with 303 additions and 179 deletions

View File

@ -15,7 +15,7 @@ INPUT_TYPE_MAPPING = {
'integer': int, 'integer': int,
'boolean': bool, 'boolean': bool,
'choice': str, 'choice': str,
'comments': list, 'comments': dict,
'colorpicker': str, 'colorpicker': str,
'datetimepicker': int, 'datetimepicker': int,
'majorityMethod': str, 'majorityMethod': str,
@ -101,17 +101,29 @@ class ConfigHandler:
raise ConfigError(e.messages[0]) raise ConfigError(e.messages[0])
if config_variable.input_type == 'comments': if config_variable.input_type == 'comments':
if not isinstance(value, list): if not isinstance(value, dict):
raise ConfigError(_('motions_comments has to be a list.')) raise ConfigError(_('motions_comments has to be a dict.'))
for comment in value: valuecopy = dict()
if not isinstance(comment, dict): for id, commentsfield in value.items():
raise ConfigError(_('Each element in motions_comments has to be a dict.')) try:
if comment.get('name') is None or comment.get('public') is None: id = int(id)
raise ConfigError(_('A name and a public property have to be given.')) except ValueError:
if not isinstance(comment['name'], str): raise ConfigError(_('Each id has to be an int.'))
raise ConfigError(_('name has to be string.'))
if not isinstance(comment['public'], bool): if id < 1:
raise ConfigError(_('public property has to be bool.')) raise ConfigError(_('Each id has to be greater then 0.'))
# Deleted commentsfields are saved as None to block the used ids
if commentsfield is not None:
if not isinstance(commentsfield, dict):
raise ConfigError(_('Each commentsfield in motions_comments has to be a dict.'))
if commentsfield.get('name') is None or commentsfield.get('public') is None:
raise ConfigError(_('A name and a public property have to be given.'))
if not isinstance(commentsfield['name'], str):
raise ConfigError(_('name has to be string.'))
if not isinstance(commentsfield['public'], bool):
raise ConfigError(_('public property has to be bool.'))
valuecopy[id] = commentsfield
value = valuecopy
if config_variable.input_type == 'logo': if config_variable.input_type == 'logo':
if not isinstance(value, dict): if not isinstance(value, dict):

View File

@ -743,15 +743,6 @@ angular.module('OpenSlidesApp.core.site', [
} }
]) ])
.filter('excludeSpecialComments', function () {
return function (comments) {
return _.filter(comments, function (comment) {
var specialComment = comment.forState || comment.forRecommendation;
return !specialComment;
});
};
})
// angular formly config options // angular formly config options
.run([ .run([
'formlyConfig', 'formlyConfig',
@ -1124,14 +1115,20 @@ angular.module('OpenSlidesApp.core.site', [
// For comments input // For comments input
$scope.addComment = function (configOption, parent) { $scope.addComment = function (configOption, parent) {
parent.value.push({ var maxId = _.max(_.keys(parent.value));
if (maxId === undefined) {
maxId = 1;
} else {
maxId = parseInt(maxId) + 1;
}
parent.value[maxId] = {
name: gettextCatalog.getString('New'), name: gettextCatalog.getString('New'),
public: false, public: false,
}); };
$scope.save(configOption, parent.value); $scope.save(configOption, parent.value);
}; };
$scope.removeComment = function (configOption, parent, index) { $scope.removeComment = function (configOption, parent, id) {
parent.value.splice(index, 1); parent.value[id] = null;
$scope.save(configOption, parent.value); $scope.save(configOption, parent.value);
}; };

View File

@ -20,8 +20,11 @@
<!-- comments --> <!-- comments -->
<div class="input-comments" ng-if="type === 'comments'"> <div class="input-comments" ng-if="type === 'comments'">
<div ng-repeat="entry in $parent.value | excludeSpecialComments" class="input-group"> <div ng-repeat="(id, field) in ($parent.value
<input ng-model="entry.name" | excludeSpecialCommentsFields
| excludeDeletedAndForbiddenCommentsFields)"
class="input-group">
<input ng-model="field.name"
ng-model-options="{debounce: 1000}" ng-model-options="{debounce: 1000}"
ng-change="save(configOption, $parent.value)" ng-change="save(configOption, $parent.value)"
class="form-control" class="form-control"
@ -29,12 +32,12 @@
type="text"> type="text">
<span class="input-group-btn"> <span class="input-group-btn">
<button type=button" class="btn btn-default" <button type=button" class="btn btn-default"
ng-click="entry.public = !entry.public; save(configOption, $parent.value);"> ng-click="field.public = !field.public; save(configOption, $parent.value);">
<i class="fa" ng-class="entry.public ? 'fa-unlock' : 'fa-lock'"></i> <i class="fa" ng-class="field.public ? 'fa-unlock' : 'fa-lock'"></i>
{{ (entry.public ? 'Public' : 'Private') | translate }} {{ (field.public ? 'Public' : 'Private') | translate }}
</button> </button>
<button type="button" class="btn btn-default" <button type="button" class="btn btn-default"
ng-click="removeComment(configOption, $parent, $index)"> ng-click="removeComment(configOption, $parent, id)">
<i class="fa fa-minus"></i> <i class="fa fa-minus"></i>
<translate>Remove</translate> <translate>Remove</translate>
</button> </button>

View File

@ -174,7 +174,7 @@ def get_config_variables():
yield ConfigVariable( yield ConfigVariable(
name='motions_comments', name='motions_comments',
default_value=[], default_value={},
input_type='comments', input_type='comments',
label='Comment fields for motions', label='Comment fields for motions',
weight=353, weight=353,

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def change_motions_comments(apps, schema_editor):
"""
Index the comments fields in the config. Changing from an array to a dict with the
ids as keys. CHange all motions from an array for comments to a dict with the comments
field id as key to link motion comments and comments fields.
"""
# We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version.
ConfigStore = apps.get_model('core', 'ConfigStore')
Motion = apps.get_model('motions', 'Motion')
try:
config_comments_fields = ConfigStore.objects.get(key='motions_comments').value
except ConfigStore.DoesNotExist:
config_comments_fields = [] # The old default: An empty list.
comments_fields = {}
for index, field in enumerate(config_comments_fields):
comments_fields[index+1] = field
max_index = len(config_comments_fields)-1
try:
db_value = ConfigStore.objects.get(key='motions_comments')
except ConfigStore.DoesNotExist:
db_value = ConfigStore(key='motions_comments')
db_value.value = comments_fields
# We cannot provide skip_autoupdate=True here, becuase this object is a fake object. It does *not*
# inherit from the RESTModelMixin, so the save() methos from base_model.py (django's default)
# gets called. This is because we are in the core app and try to save a core model. See
# comments in PR #3376.
db_value.save()
for motion in Motion.objects.all():
comments = {}
for index, comment in enumerate(motion.comments or []):
if index > max_index:
break
comments[index+1] = comment
motion.comments = comments
motion.save(skip_autoupdate=True)
class Migration(migrations.Migration):
dependencies = [
('motions', '0002_misc_features'),
]
operations = [
migrations.RunPython(
change_motions_comments
),
]

View File

@ -167,7 +167,7 @@ class Motion(RESTModelMixin, models.Model):
comments = JSONField(null=True) comments = JSONField(null=True)
""" """
Configurable fields for comments. Contains a list of strings. Configurable fields for comments.
""" """
# In theory there could be one then more agenda_item. But we support only # In theory there could be one then more agenda_item. But we support only

View File

@ -105,11 +105,15 @@ class MotionCommentsJSONSerializerField(Field):
""" """
Checks that data is a list of strings. Checks that data is a list of strings.
""" """
if type(data) is not list: if type(data) is not dict:
raise ValidationError({'detail': 'Data must be an array.'}) raise ValidationError({'detail': 'Data must be a dict.'})
for element in data: for id, comment in data.items():
if type(element) is not str: try:
raise ValidationError({'detail': 'Data must be an array of strings.'}) id = int(id)
except ValueError:
raise ValidationError({'detail': 'Id must be an int.'})
if type(comment) is not str:
raise ValidationError({'detail': 'Comment must be a string.'})
return data return data
@ -317,9 +321,9 @@ class MotionSerializer(ModelSerializer):
data['text'] = validate_html(data['text']) data['text'] = validate_html(data['text'])
if 'reason' in data: if 'reason' in data:
data['reason'] = validate_html(data['reason']) data['reason'] = validate_html(data['reason'])
validated_comments = [] validated_comments = dict()
for comment in data.get('comments', []): for id, comment in data.get('comments', {}).items():
validated_comments.append(validate_html(comment)) validated_comments[id] = validate_html(comment)
data['comments'] = validated_comments data['comments'] = validated_comments
return data return data

View File

@ -678,18 +678,32 @@ angular.module('OpenSlidesApp.motions', [
// Service for generic comment fields // Service for generic comment fields
.factory('MotionComment', [ .factory('MotionComment', [
'$filter',
'Config', 'Config',
'operator', 'operator',
'Editor', 'Editor',
function (Config, operator, Editor) { function ($filter, Config, operator, Editor) {
return { return {
getFormFields: function () { isSpecialCommentField: function (field) {
if (field) {
return field.forState || field.forRecommendation;
} else {
return false;
}
},
getCommentsFields: function () {
var fields = Config.get('motions_comments').value; var fields = Config.get('motions_comments').value;
return _.map( return $filter('excludeDeletedAndForbiddenCommentsFields')(fields);
_.filter(fields, function(element) { return !element.forState && !element.forRecommendation; }), },
function (field) { getNoSpecialCommentsFields: function () {
var fields = this.getCommentsFields();
return $filter('excludeSpecialCommentsFields')(fields);
},
getFormFields: function () {
var fields = this.getNoSpecialCommentsFields();
return _.map(fields, function (field, id) {
return { return {
key: 'comment ' + field.name, key: 'comment_' + id,
type: 'editor', type: 'editor',
templateOptions: { templateOptions: {
label: field.name, label: field.name,
@ -704,36 +718,60 @@ 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'], ... var fields = this.getCommentsFields();
var fields = Config.get('motions_comments').value; if (motion.comments) {
if (!motion.comments) { _.forEach(fields, function (field, id) {
motion.comments = []; motion['comment_' + id] = motion.comments[id];
} });
for (var i = 0; i < fields.length; i++) {
motion['comment ' + fields[i].name] = motion.comments[i];
} }
}, },
populateFieldsReverse: function (motion) { populateFieldsReverse: function (motion) {
// Reverse equivalent to populateFields. // Reverse equivalent to populateFields.
var fields = Config.get('motions_comments').value; var fields = this.getCommentsFields();
motion.comments = []; motion.comments = {};
for (var i = 0; i < fields.length; i++) { _.forEach(fields, function (field, id) {
motion.comments.push(motion['comment ' + fields[i].name] || ''); motion.comments[id] = motion['comment_' + id] || '';
} });
}, },
getFieldNameForFlag: function (flag) { getFieldIdForFlag: function (flag) {
var fields = Config.get('motions_comments').value; var fields = this.getCommentsFields();
var fieldName = ''; return _.findKey(fields, [flag, true]);
var index = _.findIndex(fields, [flag, true]);
if (index > -1) {
fieldName = fields[index].name;
}
return fieldName;
}, },
}; };
} }
]) ])
.filter('excludeSpecialCommentsFields', [
'MotionComment',
function (MotionComment) {
return function (commentsFields) {
var withoutSpecialCommentsFields = {};
_.forEach(commentsFields, function (field, id) {
if (!MotionComment.isSpecialCommentField(field)) {
withoutSpecialCommentsFields[id] = field;
}
});
return withoutSpecialCommentsFields;
};
}
])
.filter('excludeDeletedAndForbiddenCommentsFields', [
'MotionComment',
'operator',
function (MotionComment, operator) {
return function (commentsFields) {
var withoutDeletedAndForbiddenCommentsFields = {};
_.forEach(commentsFields, function (field, id) {
if (field && (field.public || operator.hasPerms('motions.can_see_and_manage_comments'))) {
withoutDeletedAndForbiddenCommentsFields[id] = field;
}
});
return withoutDeletedAndForbiddenCommentsFields;
};
}
])
.factory('Category', [ .factory('Category', [
'DS', 'DS',
function(DS) { function(DS) {

View File

@ -14,7 +14,9 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
'FileSaver', 'FileSaver',
'lineNumberingService', 'lineNumberingService',
'Html2DocxConverter', 'Html2DocxConverter',
function ($http, $q, operator, Config, Category, gettextCatalog, FileSaver, lineNumberingService, Html2DocxConverter) { 'MotionComment',
function ($http, $q, operator, Config, Category, gettextCatalog, FileSaver, lineNumberingService,
Html2DocxConverter, MotionComment) {
var PAGEBREAK = '<w:p><w:r><w:br w:type="page" /></w:r></w:p>'; var PAGEBREAK = '<w:p><w:r><w:br w:type="page" /></w:r></w:p>';
@ -153,19 +155,15 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
}; };
var getMotionComments = function (motion) { var getMotionComments = function (motion) {
var fields = Config.get('motions_comments').value; var fields = MotionComment.getNoSpecialCommentsFields();
var canSeeComment = function (index) {
var specialComment = fields[index].forState || fields[index].forRecommendation;
return (fields[index].public || operator.hasPerms('motions.can_manage')) && !specialComment;
};
var comments = []; var comments = [];
for (var i = 0; i < fields.length; i++) { _.forEach(fields, function (field, id) {
if (motion.comments[i] && canSeeComment(i)) { if (motion.comments[id]) {
var title = gettextCatalog.getString('Comment') + ' ' + fields[i].name; var title = gettextCatalog.getString('Comment') + ' ' + field.name;
if (!fields[i].public) { if (!field.public) {
title += ' (' + gettextCatalog.getString('internal') + ')'; title += ' (' + gettextCatalog.getString('internal') + ')';
} }
var comment = motion.comments[i]; var comment = motion.comments[id];
if (comment.indexOf('<p>') !== 0) { if (comment.indexOf('<p>') !== 0) {
comment = '<p>' + comment + '</p>'; comment = '<p>' + comment + '</p>';
} }
@ -174,7 +172,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
comment: comment, comment: comment,
}); });
} }
} });
return comments; return comments;
}; };

View File

@ -134,14 +134,14 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
editors: [] editors: []
}; };
var options = Editor.getOptions('inline', 'YOffset'); var options = Editor.getOptions('inline', 'YOffset');
_.forEach($scope.commentsFieldsNoSpecialComments, function (field) { _.forEach($scope.noSpecialCommentsFields, function (field, id) {
var inlineEditing = MotionInlineEditing.createInstance($scope, motion, var inlineEditing = MotionInlineEditing.createInstance($scope, motion,
'view-original-comment-inline-editor-' + field.name, false, options, 'view-original-comment-inline-editor-' + id, false, options,
function (obj) { function (obj) {
return motion['comment ' + field.name]; return motion['comment_' + id];
}, },
function (obj) { function (obj) {
motion['comment ' + field.name] = obj.editor.getData(); motion['comment_' + id] = obj.editor.getData();
} }
); );
commentsInlineEditing.editors.push(inlineEditing); commentsInlineEditing.editors.push(inlineEditing);

View File

@ -15,7 +15,9 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'Category', 'Category',
'Config', 'Config',
'Motion', 'Motion',
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, HTMLValidizer, Category, Config, Motion) { 'MotionComment',
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, HTMLValidizer,
Category, Config, Motion, MotionComment) {
/** /**
* Provides the content as JS objects for Motions in pdfMake context * Provides the content as JS objects for Motions in pdfMake context
* @constructor * @constructor
@ -27,10 +29,19 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
var converter; var converter;
// Query all image sources from motion text and reason // Query all image sources from motion text and reason
// TODO: Do we need images for comments here too??
var getImageSources = function () { var getImageSources = function () {
var text = motion.getTextByMode(changeRecommendationMode, null); var text = motion.getTextByMode(changeRecommendationMode, null);
var content = HTMLValidizer.validize(text) + HTMLValidizer.validize(motion.getReason()); var reason = motion.getReason();
var comments = '';
if (includeComments) {
var fields = MotionComment.getNoSpecialCommentsFields();
_.forEach(fields, function (field, id) {
if (motion.comments[id]) {
comments += HTMLValidizer.validize(motion.comments[id]);
}
});
}
var content = HTMLValidizer.validize(text) + HTMLValidizer.validize(motion.getReason()) + comments;
var map = Function.prototype.call.bind([].map); var map = Function.prototype.call.bind([].map);
return map($(content).find('img'), function(element) { return map($(content).find('img'), function(element) {
return element.getAttribute('src'); return element.getAttribute('src');
@ -298,16 +309,12 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
// motion comments handling // motion comments handling
var motionComments = function () { var motionComments = function () {
if (includeComments) { if (includeComments) {
var fields = Config.get('motions_comments').value; var fields = MotionComment.getNoSpecialCommentsFields();
var canSeeComment = function (index) {
var specialComment = fields[index].forState || fields[index].forRecommendation;
return (fields[index].public || operator.hasPerms('motions.can_manage')) && !specialComment;
};
var comments = []; var comments = [];
for (var i = 0; i < fields.length; i++) { _.forEach(fields, function (field, id) {
if (motion.comments[i] && canSeeComment(i)) { if (motion.comments[id]) {
var title = gettextCatalog.getString('Comment') + ' ' + fields[i].name; var title = gettextCatalog.getString('Comment') + ' ' + field.name;
if (!fields[i].public) { if (!field.public) {
title += ' (' + gettextCatalog.getString('internal') + ')'; title += ' (' + gettextCatalog.getString('internal') + ')';
} }
comments.push({ comments.push({
@ -315,9 +322,9 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
style: 'heading3', style: 'heading3',
marginTop: 25, marginTop: 25,
}); });
comments.push(converter.convertHTML(motion.comments[i])); comments.push(converter.convertHTML(motion.comments[id]));
} }
} });
return comments; return comments;
} }
}; };
@ -859,12 +866,13 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'PdfCreate', 'PdfCreate',
'PDFLayout', 'PDFLayout',
'PersonalNoteManager', 'PersonalNoteManager',
'MotionComment',
'Messaging', 'Messaging',
'FileSaver', 'FileSaver',
function ($http, $q, operator, Config, gettextCatalog, MotionChangeRecommendation, HTMLValidizer, function ($http, $q, operator, Config, gettextCatalog, MotionChangeRecommendation, HTMLValidizer,
PdfMakeConverter, MotionContentProvider, MotionCatalogContentProvider, PdfMakeDocumentProvider, PdfMakeConverter, MotionContentProvider, MotionCatalogContentProvider, PdfMakeDocumentProvider,
PollContentProvider, PdfMakeBallotPaperProvider, MotionPartialContentProvider, PdfCreate, PollContentProvider, PdfMakeBallotPaperProvider, MotionPartialContentProvider, PdfCreate,
PDFLayout, PersonalNoteManager, Messaging, FileSaver) { PDFLayout, PersonalNoteManager, MotionComment, Messaging, FileSaver) {
return { return {
getDocumentProvider: function (motions, params, singleMotion) { getDocumentProvider: function (motions, params, singleMotion) {
params = _.clone(params || {}); // Clone this to avoid sideeffects. params = _.clone(params || {}); // Clone this to avoid sideeffects.
@ -1008,24 +1016,20 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
}); });
}, },
exportComments: function (motion, filename) { exportComments: function (motion, filename) {
var fields = Config.get('motions_comments').value; var fields = MotionComment.getNoSpecialCommentsFields();
var canSeeComment = function (index) {
var specialComment = fields[index].forState || fields[index].forRecommendation;
return (fields[index].public || operator.hasPerms('motions.can_manage')) && !specialComment;
};
var content = []; var content = [];
for (var i = 0; i < fields.length; i++) { _.forEach(fields, function (field, id) {
if (motion.comments[i] && canSeeComment(i)) { if (motion.comments[id]) {
var title = gettextCatalog.getString('Comment') + ' ' + fields[i].name; var title = gettextCatalog.getString('Comment') + ' ' + field.name;
if (!fields[i].public) { if (!field.public) {
title += ' (' + gettextCatalog.getString('internal') + ')'; title += ' (' + gettextCatalog.getString('internal') + ')';
} }
content.push({ content.push({
heading: title, heading: title,
text: motion.comments[i], text: motion.comments[id],
}); });
} }
} });
MotionPartialContentProvider.createInstance(motion, content).then(function (contentProvider) { MotionPartialContentProvider.createInstance(motion, content).then(function (contentProvider) {
PdfMakeDocumentProvider.createInstance(contentProvider).then(function (documentProvider) { PdfMakeDocumentProvider.createInstance(contentProvider).then(function (documentProvider) {
PdfCreate.download(documentProvider.getDocument(), filename); PdfCreate.download(documentProvider.getDocument(), filename);

View File

@ -910,11 +910,10 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.alert = {}; $scope.alert = {};
// Motion comments // Motion comments
$scope.commentsFields = Config.get('motions_comments').value; $scope.noSpecialCommentsFields = MotionComment.getNoSpecialCommentsFields();
$scope.commentsFieldsNoSpecialComments = _.filter($scope.commentsFields, function (field) { $scope.showCommentsFilter = function () {
var specialComment = field.forState || field.forRecommendation; return _.keys($scope.noSpecialCommentsFields).length > 0;
return !specialComment; };
});
// collect all states and all recommendations of all workflows // collect all states and all recommendations of all workflows
$scope.states = []; $scope.states = [];
@ -1289,13 +1288,10 @@ angular.module('OpenSlidesApp.motions.site', [
}); });
} }
}; };
$scope.commentsFields = Config.get('motions_comments').value; $scope.commentsFields = MotionComment.getCommentsFields();
$scope.commentsFieldsNoSpecialComments = _.filter($scope.commentsFields, function (field) { $scope.noSpecialCommentsFields = MotionComment.getNoSpecialCommentsFields();
var specialComment = field.forState || field.forRecommendation; $scope.commentFieldForStateId = MotionComment.getFieldIdForFlag('forState');
return !specialComment; $scope.commentFieldForRecommendationId = MotionComment.getFieldIdForFlag('forRecommendation');
});
$scope.commentFieldForState = MotionComment.getFieldNameForFlag('forState');
$scope.commentFieldForRecommendation = MotionComment.getFieldNameForFlag('forRecommendation');
$scope.version = motion.active_version; $scope.version = motion.active_version;
$scope.isCollapsed = true; $scope.isCollapsed = true;
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value; $scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
@ -1402,14 +1398,14 @@ angular.module('OpenSlidesApp.motions.site', [
// save additional state field // save additional state field
$scope.saveAdditionalStateField = function (stateExtension) { $scope.saveAdditionalStateField = function (stateExtension) {
if (stateExtension) { if (stateExtension) {
motion["comment " + $scope.commentFieldForState] = stateExtension; motion['comment_' + $scope.commentFieldForStateId] = stateExtension;
$scope.save(motion); $scope.save(motion);
} }
}; };
// save additional recommendation field // save additional recommendation field
$scope.saveAdditionalRecommendationField = function (recommendationExtension) { $scope.saveAdditionalRecommendationField = function (recommendationExtension) {
if (recommendationExtension) { if (recommendationExtension) {
motion["comment " + $scope.commentFieldForRecommendation] = recommendationExtension; motion['comment_' + $scope.commentFieldForRecommendationId] = recommendationExtension;
$scope.save(motion); $scope.save(motion);
} }
}; };
@ -1471,18 +1467,9 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.showVersion({id: motion.active_version}); $scope.showVersion({id: motion.active_version});
}); });
}; };
// check if user is allowed to see at least one comment field // check if there is at least one comment field
$scope.isAllowedToSeeCommentField = function () { $scope.commentFieldsAvailable = function () {
var isAllowed = false; return _.keys($scope.noSpecialCommentsFields).length > 0;
if ($scope.commentsFields.length > 0) {
isAllowed = operator.hasPerms('motions.can_see_and_manage_comments') || _.find(
$scope.commentsFields,
function(field) {
return field.public && !field.forState && !field.forRecommendation;
}
);
}
return Boolean(isAllowed);
}; };
// personal note // personal note
// For pinning the personal note container we need to adjust the width with JS. We // For pinning the personal note container we need to adjust the width with JS. We

View File

@ -161,10 +161,10 @@
</div> </div>
<div os-perms="motions.can_manage" class="input-group spacer" <div os-perms="motions.can_manage" class="input-group spacer"
ng-show="motion.state.show_state_extension_field"> ng-show="motion.state.show_state_extension_field">
<label class="sr-only" for="stateExtensionField">{{ commentFieldForState }}</label> <label class="sr-only" for="stateExtensionField">{{ commentsFields[commentFieldForStateId] }}</label>
<input type="text" ng-model="stateExtension" <input type="text" ng-model="stateExtension"
id="stateNameExtensionField" class="form-control input-sm" id="stateNameExtensionField" class="form-control input-sm"
placeholder="{{ commentFieldForState }}"> placeholder="{{ commentsFields[commentFieldForStateId] }}">
<span class="input-group-btn"> <span class="input-group-btn">
<button ng-click="saveAdditionalStateField(stateExtension)" class="btn btn-default btn-sm"> <button ng-click="saveAdditionalStateField(stateExtension)" class="btn btn-default btn-sm">
<i class="fa fa-check"></i> <i class="fa fa-check"></i>
@ -203,10 +203,12 @@
</div> </div>
<div class="input-group spacer" <div class="input-group spacer"
ng-if="motion.recommendation.show_recommendation_extension_field"> ng-if="motion.recommendation.show_recommendation_extension_field">
<label class="sr-only" for="recommendationExtensionField">{{ commentFieldForRecommendation }}</label> <label class="sr-only" for="recommendationExtensionField">
{{ commentsFields[commentFieldForRecommendationId] }}
</label>
<input type="text" ng-model="recommendationExtension" <input type="text" ng-model="recommendationExtension"
id="recommendationExtensionField" class="form-control input-sm" id="recommendationExtensionField" class="form-control input-sm"
placeholder="{{ commentFieldForRecommendation }}"> placeholder="{{ commentsFields[commentFieldForRecommendationId] }}">
<span class="input-group-btn"> <span class="input-group-btn">
<button ng-click="saveAdditionalRecommendationField(recommendationExtension)" class="btn btn-default btn-sm"> <button ng-click="saveAdditionalRecommendationField(recommendationExtension)" class="btn btn-default btn-sm">
<i class="fa fa-check"></i> <i class="fa fa-check"></i>

View File

@ -1,4 +1,4 @@
<div class="details" ng-if="isAllowedToSeeCommentField() && commentsFieldsNoSpecialComments.length"> <div class="details" ng-if="commentFieldsAvailable()">
<div class="row"> <div class="row">
<!-- inline editing toolbar --> <!-- inline editing toolbar -->
<div class="motion-toolbar"> <div class="motion-toolbar">
@ -25,15 +25,13 @@
</div> </div>
<!-- comment fields --> <!-- comment fields -->
<div class="col-sm-12"> <div class="col-sm-12">
<div ng-repeat="field in commentsFieldsNoSpecialComments"> <div ng-repeat="(id, field) in noSpecialCommentsFields">
<div ng-if="operator.hasPerms('motions.can_see_and_manage_comments') && !field.forState && !field.forRecommendation"> <h4>
<h4> {{ field.name }}
{{ field.name }} <span ng-if="!field.public" class="label label-warning" translate>internal</span>
<span ng-if="!field.public" class="label label-warning" translate>internal</span> </h4>
</h4> <div id="view-original-comment-inline-editor-{{ id }}" style="min-height: 14px;"
<div id="view-original-comment-inline-editor-{{ field.name }}" style="min-height: 14px;" ng-bind-html="motion.comments[id] | trusted" contenteditable="{{ commentsInlineEditing.editors[$index].isEditable }}"></div>
ng-bind-html="motion.comments[$index] | trusted" contenteditable="{{ commentsInlineEditing.editors[$index].isEditable }}"></div>
</div>
</div> </div>
<!-- save toolbar --> <!-- save toolbar -->
<div class="motion-save-toolbar" ng-class="{ 'visible': commentsInlineEditing.saveToolbarVisible() }"> <div class="motion-save-toolbar" ng-class="{ 'visible': commentsInlineEditing.saveToolbarVisible() }">

View File

@ -169,30 +169,28 @@
</ul> </ul>
</span> </span>
<!-- Comment filter --> <!-- Comment filter -->
<span os-perms="motions.can_see_and_manage_comments"> <span uib-dropdown ng-if="showCommentsFilter()">
<span uib-dropdown ng-if="commentsFieldsNoSpecialComments.length > 0"> <span class="pointer" id="dropdownComment" uib-dropdown-toggle
<span class="pointer" id="dropdownComment" uib-dropdown-toggle ng-class="{'bold': filter.multiselectFilters.comment.length > 0, 'disabled': isSelectMode}"
ng-class="{'bold': filter.multiselectFilters.comment.length > 0, 'disabled': isSelectMode}" ng-disabled="isSelectMode">
ng-disabled="isSelectMode"> <translate>Comment</translate>
<translate>Comment</translate> <span class="caret"></span>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownComment">
<li ng-repeat="commentsField in commentsFieldsNoSpecialComments">
<a href ng-click="filter.operateMultiselectFilter('comment', commentsField.name, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.comment.indexOf(commentsField.name) > -1"></i>
{{ commentsField.name }} <translate>is set</translate>
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('comment', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.comment.indexOf(-1) > -1"></i>
<translate>No comments set</translate>
</a>
</li>
</ul>
</span> </span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownComment">
<li ng-repeat="commentsField in noSpecialCommentsFields">
<a href ng-click="filter.operateMultiselectFilter('comment', commentsField.name, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.comment.indexOf(commentsField.name) > -1"></i>
{{ commentsField.name }} <translate>is set</translate>
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('comment', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.comment.indexOf(-1) > -1"></i>
<translate>No comments set</translate>
</a>
</li>
</ul>
</span> </span>
<!-- recommendation filter --> <!-- recommendation filter -->
<span uib-dropdown ng-if="config('motions_recommendations_by') != ''"> <span uib-dropdown ng-if="config('motions_recommendations_by') != ''">

View File

@ -238,10 +238,9 @@ class CreateMotion(TestCase):
self.assertEqual(motion.tags.get().name, 'test_tag_iRee3kiecoos4rorohth') self.assertEqual(motion.tags.get().name, 'test_tag_iRee3kiecoos4rorohth')
def test_with_multiple_comments(self): def test_with_multiple_comments(self):
config['motions_comments'] = [ comments = {
{'name': 'comment1', 'public': True}, '1': 'comemnt1_sdpoiuffo3%7dwDwW)',
{'name': 'comment2', 'public': False}] '2': 'comment2_iusd_D/TdskDWH(5DWas46WAd078'}
comments = ['comemnt1_sdpoiuffo3%7dwDwW)', 'comment2_iusd_D/TdskDWH(5DWas46WAd078']
response = self.client.post( response = self.client.post(
reverse('motion-list'), reverse('motion-list'),
{'title': 'title_test_sfdAaufd56HR7sd5FDq7av', {'title': 'title_test_sfdAaufd56HR7sd5FDq7av',
@ -252,6 +251,31 @@ class CreateMotion(TestCase):
motion = Motion.objects.get() motion = Motion.objects.get()
self.assertEqual(motion.comments, comments) self.assertEqual(motion.comments, comments)
def test_wrong_comment_format(self):
comments = [
'comemnt1_wpcjlwgj$§ks)skj2LdmwKDWSLw6',
'comment2_dq2Wd)Jwdlmm:,w82DjwQWSSiwjd']
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_400_BAD_REQUEST)
self.assertEqual(response.data, {'comments': {'detail': 'Data must be a dict.'}})
def test_wrong_comment_id(self):
comment = {
'string': 'comemnt1_wpcjlwgj$§ks)skj2LdmwKDWSLw6'}
response = self.client.post(
reverse('motion-list'),
{'title': 'title_test_sfdAaufd56HR7sd5FDq7av',
'text': 'text_test_fiuhefF86()ew1Ef346AF6W',
'comments': comment},
format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'comments': {'detail': 'Id must be an int.'}})
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.
@ -298,13 +322,12 @@ class CreateMotion(TestCase):
reverse('motion-list'), reverse('motion-list'),
{'title': 'test_title_peiJozae0luew9EeL8bo', {'title': 'test_title_peiJozae0luew9EeL8bo',
'text': 'test_text_eHohS8ohr5ahshoah8Oh', 'text': 'test_text_eHohS8ohr5ahshoah8Oh',
'comments': ['comment_for_field_one__xiek1Euhae9xah2wuuraaaa'], 'comments': {'1': 'comment_for_field_one__xiek1Euhae9xah2wuuraaaa'}},
'comment_field_one': 'comment_for_field_one__xiek1Euhae9xah2wuuraaaa'},
format='json', format='json',
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Motion.objects.get().comments, ['comment_for_field_one__xiek1Euhae9xah2wuuraaaa']) self.assertEqual(Motion.objects.get().comments, {'1': 'comment_for_field_one__xiek1Euhae9xah2wuuraaaa'})
def test_amendment_motion(self): def test_amendment_motion(self):
""" """