Rework on motion comments (fixes #3350).
This commit is contained in:
parent
c9ad15621c
commit
288a706d01
@ -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):
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
60
openslides/motions/migrations/0003_motion_comments.py
Normal file
60
openslides/motions/migrations/0003_motion_comments.py
Normal 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
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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() }">
|
||||
|
@ -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') != ''">
|
||||
|
@ -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):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user