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,
'boolean': bool,
'choice': str,
'comments': list,
'comments': dict,
'colorpicker': str,
'datetimepicker': int,
'majorityMethod': str,
@ -101,17 +101,29 @@ class ConfigHandler:
raise ConfigError(e.messages[0])
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.'))
if not isinstance(value, dict):
raise ConfigError(_('motions_comments has to be a dict.'))
valuecopy = dict()
for id, commentsfield in value.items():
try:
id = int(id)
except ValueError:
raise ConfigError(_('Each id has to be an int.'))
if id < 1:
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 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
.run([
'formlyConfig',
@ -1124,14 +1115,20 @@ angular.module('OpenSlidesApp.core.site', [
// For comments input
$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'),
public: false,
});
};
$scope.save(configOption, parent.value);
};
$scope.removeComment = function (configOption, parent, index) {
parent.value.splice(index, 1);
$scope.removeComment = function (configOption, parent, id) {
parent.value[id] = null;
$scope.save(configOption, parent.value);
};

View File

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

View File

@ -174,7 +174,7 @@ def get_config_variables():
yield ConfigVariable(
name='motions_comments',
default_value=[],
default_value={},
input_type='comments',
label='Comment fields for motions',
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)
"""
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

View File

@ -105,11 +105,15 @@ class MotionCommentsJSONSerializerField(Field):
"""
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.'})
if type(data) is not dict:
raise ValidationError({'detail': 'Data must be a dict.'})
for id, comment in data.items():
try:
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
@ -317,9 +321,9 @@ class MotionSerializer(ModelSerializer):
data['text'] = validate_html(data['text'])
if 'reason' in data:
data['reason'] = validate_html(data['reason'])
validated_comments = []
for comment in data.get('comments', []):
validated_comments.append(validate_html(comment))
validated_comments = dict()
for id, comment in data.get('comments', {}).items():
validated_comments[id] = validate_html(comment)
data['comments'] = validated_comments
return data

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -161,10 +161,10 @@
</div>
<div os-perms="motions.can_manage" class="input-group spacer"
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"
id="stateNameExtensionField" class="form-control input-sm"
placeholder="{{ commentFieldForState }}">
placeholder="{{ commentsFields[commentFieldForStateId] }}">
<span class="input-group-btn">
<button ng-click="saveAdditionalStateField(stateExtension)" class="btn btn-default btn-sm">
<i class="fa fa-check"></i>
@ -203,10 +203,12 @@
</div>
<div class="input-group spacer"
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"
id="recommendationExtensionField" class="form-control input-sm"
placeholder="{{ commentFieldForRecommendation }}">
placeholder="{{ commentsFields[commentFieldForRecommendationId] }}">
<span class="input-group-btn">
<button ng-click="saveAdditionalRecommendationField(recommendationExtension)" class="btn btn-default btn-sm">
<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">
<!-- inline editing toolbar -->
<div class="motion-toolbar">
@ -25,15 +25,13 @@
</div>
<!-- comment fields -->
<div class="col-sm-12">
<div ng-repeat="field in commentsFieldsNoSpecialComments">
<div ng-if="operator.hasPerms('motions.can_see_and_manage_comments') && !field.forState && !field.forRecommendation">
<h4>
{{ field.name }}
<span ng-if="!field.public" class="label label-warning" translate>internal</span>
</h4>
<div id="view-original-comment-inline-editor-{{ field.name }}" style="min-height: 14px;"
ng-bind-html="motion.comments[$index] | trusted" contenteditable="{{ commentsInlineEditing.editors[$index].isEditable }}"></div>
</div>
<div ng-repeat="(id, field) in noSpecialCommentsFields">
<h4>
{{ field.name }}
<span ng-if="!field.public" class="label label-warning" translate>internal</span>
</h4>
<div id="view-original-comment-inline-editor-{{ id }}" style="min-height: 14px;"
ng-bind-html="motion.comments[id] | trusted" contenteditable="{{ commentsInlineEditing.editors[$index].isEditable }}"></div>
</div>
<!-- save toolbar -->
<div class="motion-save-toolbar" ng-class="{ 'visible': commentsInlineEditing.saveToolbarVisible() }">

View File

@ -169,30 +169,28 @@
</ul>
</span>
<!-- Comment filter -->
<span os-perms="motions.can_see_and_manage_comments">
<span uib-dropdown ng-if="commentsFieldsNoSpecialComments.length > 0">
<span class="pointer" id="dropdownComment" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.comment.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Comment</translate>
<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 uib-dropdown ng-if="showCommentsFilter()">
<span class="pointer" id="dropdownComment" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.comment.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Comment</translate>
<span class="caret"></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>
<!-- recommendation filter -->
<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')
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']
comments = {
'1': 'comemnt1_sdpoiuffo3%7dwDwW)',
'2': 'comment2_iusd_D/TdskDWH(5DWas46WAd078'}
response = self.client.post(
reverse('motion-list'),
{'title': 'title_test_sfdAaufd56HR7sd5FDq7av',
@ -252,6 +251,31 @@ class CreateMotion(TestCase):
motion = Motion.objects.get()
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):
"""
Test to create a motion with a specific workflow.
@ -298,13 +322,12 @@ class CreateMotion(TestCase):
reverse('motion-list'),
{'title': 'test_title_peiJozae0luew9EeL8bo',
'text': 'test_text_eHohS8ohr5ahshoah8Oh',
'comments': ['comment_for_field_one__xiek1Euhae9xah2wuuraaaa'],
'comment_field_one': 'comment_for_field_one__xiek1Euhae9xah2wuuraaaa'},
'comments': {'1': 'comment_for_field_one__xiek1Euhae9xah2wuuraaaa'}},
format='json',
)
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):
"""