diff --git a/CHANGELOG b/CHANGELOG
index 71c6fba32..4fd085a01 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,7 @@ https://openslides.org/
Version 2.2 (unreleased)
==========================
+[https://github.com/OpenSlides/OpenSlides/milestones/2.2]
Agenda:
- Fixed wrong sorting of last speakers [#3193].
@@ -13,6 +14,7 @@ Agenda:
Motions:
- New export dialog [#3185].
+- New feature: Personal notes for motions [#3190].
- Fixed issue when creating/deleting motion comment fields in the
settings [#3187].
- Fixed empty motion comment field in motion update form [#3194].
diff --git a/openslides/global_settings.py b/openslides/global_settings.py
index e47f8fe8a..f5a7124cf 100644
--- a/openslides/global_settings.py
+++ b/openslides/global_settings.py
@@ -92,6 +92,8 @@ STATICFILES_DIRS = [
AUTH_USER_MODEL = 'users.User'
+AUTH_PERSONAL_NOTE_MODEL = 'users.PersonalNote'
+
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py
index 0471bc52d..0fc996d24 100644
--- a/openslides/motions/access_permissions.py
+++ b/openslides/motions/access_permissions.py
@@ -64,6 +64,14 @@ class MotionAccessPermissions(BaseAccessPermissions):
except IndexError:
# No data in range. Just do nothing.
pass
+ # Now filter personal notes.
+ data = data.copy()
+ data['personal_notes'] = []
+ if user is not None:
+ for personal_note in full_data.get('personal_notes', []):
+ if personal_note.get('user_id') == user.id:
+ data['personal_notes'].append(personal_note)
+ break
return data
def get_projector_data(self, full_data):
diff --git a/openslides/motions/models.py b/openslides/motions/models.py
index 4c72872f3..06f1b29a3 100644
--- a/openslides/motions/models.py
+++ b/openslides/motions/models.py
@@ -50,7 +50,8 @@ class MotionManager(models.Manager):
'attachments',
'tags',
'submitters',
- 'supporters'))
+ 'supporters',
+ 'personal_notes'))
class Motion(RESTModelMixin, models.Model):
@@ -170,6 +171,8 @@ class Motion(RESTModelMixin, models.Model):
Configurable fields for comments. Contains a list of strings.
"""
+ personal_notes = GenericRelation(settings.AUTH_PERSONAL_NOTE_MODEL, related_name='motions')
+
# In theory there could be one then more agenda_item. But we support only
# one. See the property agenda_item.
agenda_items = GenericRelation(Item, related_name='motions')
@@ -639,6 +642,14 @@ class Motion(RESTModelMixin, models.Model):
"""
return self.agenda_item.pk
+ def set_personal_note(self, user, note=None, star=None, skip_autoupdate=False):
+ """
+ Saves or overrides a personal note to this motion for a given user.
+ """
+ user.set_personal_note(self, note, star)
+ if not skip_autoupdate:
+ inform_changed_data(self)
+
def write_log(self, message_list, person=None, skip_autoupdate=False):
"""
Write a log message.
diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py
index 1dabd9a05..79997fa30 100644
--- a/openslides/motions/serializers.py
+++ b/openslides/motions/serializers.py
@@ -270,6 +270,7 @@ class MotionSerializer(ModelSerializer):
"""
active_version = PrimaryKeyRelatedField(read_only=True)
comments = MotionCommentsJSONSerializerField(required=False)
+ personal_notes = SerializerMethodField()
log_messages = MotionLogSerializer(many=True, read_only=True)
polls = MotionPollSerializer(many=True, read_only=True)
reason = CharField(allow_blank=True, required=False, write_only=True)
@@ -300,6 +301,7 @@ class MotionSerializer(ModelSerializer):
'submitters',
'supporters',
'comments',
+ 'personal_notes',
'state',
'state_required_permission_to_see',
'workflow_id',
@@ -386,6 +388,12 @@ class MotionSerializer(ModelSerializer):
return motion
+ def get_personal_notes(self, motion):
+ """
+ Returns the personal notes of all users.
+ """
+ return [personal_note.get_data() for personal_note in motion.personal_notes.all()]
+
def get_state_required_permission_to_see(self, motion):
"""
Returns the permission (as string) that is required for non
diff --git a/openslides/motions/static/js/motions/motion-services.js b/openslides/motions/static/js/motions/motion-services.js
index cb9fd768e..3446a5e54 100644
--- a/openslides/motions/static/js/motions/motion-services.js
+++ b/openslides/motions/static/js/motions/motion-services.js
@@ -4,6 +4,13 @@
angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions', 'OpenSlidesApp.motions.lineNumbering'])
+/* Generic inline editing factory.
+ *
+ * getOriginalData: Function that should return the editor data. The editor object is passed.
+ * saveData: Function that is called whith the editor object as argument. This function
+ * should prepare the save. If the function returns true, the save process won't be
+ * continued. Else a patch request is send.
+ */
.factory('MotionInlineEditing', [
'Editor',
'Motion',
@@ -85,30 +92,31 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
};
obj.save = function () {
- saveData(obj);
- obj.disable();
+ if (!saveData(obj)) {
+ obj.disable();
- Motion.inject(motion);
- // save change motion object on server
- Motion.save(motion, {method: 'PATCH'}).then(
- function (success) {
- if (versioning) {
- $scope.showVersion(motion.getVersion(-1));
+ Motion.inject(motion);
+ // save change motion object on server
+ Motion.save(motion, {method: 'PATCH'}).then(
+ function (success) {
+ if (versioning) {
+ $scope.showVersion(motion.getVersion(-1));
+ }
+ obj.revert();
+ },
+ function (error) {
+ // save error: revert all changes by restore
+ // (refresh) original motion object from server
+ Motion.refresh(motion);
+ obj.revert();
+ var message = '';
+ for (var e in error.data) {
+ message += e + ': ' + error.data[e] + ' ';
+ }
+ $scope.alert = {type: 'danger', msg: message, show: true};
}
- obj.revert();
- },
- function (error) {
- // save error: revert all changes by restore
- // (refresh) original motion object from server
- Motion.refresh(motion);
- obj.revert();
- var message = '';
- for (var e in error.data) {
- message += e + ': ' + error.data[e] + ' ';
- }
- $scope.alert = {type: 'danger', msg: message, show: true};
- }
- );
+ );
+ }
};
return obj;
diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js
index d06d080f8..8f6baa1bd 100644
--- a/openslides/motions/static/js/motions/site.js
+++ b/openslides/motions/static/js/motions/site.js
@@ -854,6 +854,7 @@ angular.module('OpenSlidesApp.motions.site', [
'$http',
'gettext',
'gettextCatalog',
+ 'operator',
'ngDialog',
'MotionForm',
'Motion',
@@ -871,10 +872,9 @@ angular.module('OpenSlidesApp.motions.site', [
'osTableSort',
'MotionExportForm',
'MotionPdfExport',
- function($scope, $state, $http, gettext, gettextCatalog, ngDialog, MotionForm, Motion, MotionComment,
- Category, Config, Tag, Workflow, User, Agenda, MotionBlock, Projector,
+ function($scope, $state, $http, gettext, gettextCatalog, operator, ngDialog, MotionForm, Motion,
+ MotionComment, Category, Config, Tag, Workflow, User, Agenda, MotionBlock, Projector,
ProjectionDefault, osTableFilter, osTableSort, MotionExportForm, MotionPdfExport) {
- Motion.bindAll({}, $scope, 'motions');
Category.bindAll({}, $scope, 'categories');
MotionBlock.bindAll({}, $scope, 'motionBlocks');
Tag.bindAll({}, $scope, 'tags');
@@ -889,6 +889,18 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
+ $scope.$watch(function () {
+ return Motion.lastModified();
+ }, function () {
+ $scope.motions = Motion.getAll();
+ _.forEach($scope.motions, function (motion) {
+ motion.personalNote = _.find(motion.personal_notes, function (note) {
+ return note.user_id === operator.user.id;
+ });
+ // For filtering, we cannot filter for .personalNote.star
+ motion.star = motion.personalNote ? motion.personalNote.star : false;
+ });
+ });
$scope.alert = {};
// collect all states and all recommendations of all workflows
@@ -939,6 +951,14 @@ angular.module('OpenSlidesApp.motions.site', [
tag: [],
recommendation: [],
};
+ $scope.filter.booleanFilters = {
+ isFavorite: {
+ value: undefined,
+ displayName: gettext('Favorite'),
+ choiceYes: gettext('Is favorite'),
+ choiceNo: gettext('Is not favorite'),
+ },
+ };
}
updateStateFilter();
$scope.filter.propertyList = ['identifier', 'origin'];
@@ -1062,6 +1082,16 @@ angular.module('OpenSlidesApp.motions.site', [
}
$scope.save(motion);
};
+ $scope.toggleStar = function (motion) {
+ if (motion.personalNote) {
+ motion.personalNote.star = !motion.personalNote.star;
+ } else {
+ motion.personalNote = {star: true};
+ }
+ $http.put('/rest/motions/motion/' + motion.id + '/set_personal_note/',
+ motion.personalNote
+ );
+ };
// open new/edit dialog
$scope.openDialog = function (motion) {
@@ -1198,6 +1228,9 @@ angular.module('OpenSlidesApp.motions.site', [
}, function () {
$scope.motion = Motion.get(motionId);
MotionComment.populateFields($scope.motion);
+ $scope.motion.personalNote = _.find($scope.motion.personal_notes, function (note) {
+ return note.user_id === operator.user.id;
+ });
});
$scope.projectionModes = [
{mode: 'original',
@@ -1419,6 +1452,31 @@ angular.module('OpenSlidesApp.motions.site', [
}
return Boolean(isAllowed);
};
+ // personal note
+ $scope.toggleStar = function () {
+ if ($scope.motion.personalNote) {
+ $scope.motion.personalNote.star = !$scope.motion.personalNote.star;
+ } else {
+ $scope.motion.personalNote = {star: true};
+ }
+ $http.put('/rest/motions/motion/' + $scope.motion.id + '/set_personal_note/',
+ $scope.motion.personalNote
+ );
+ };
+ $scope.changePN = function () {
+ };
+
+ // personal note
+ $scope.toggleStar = function () {
+ if ($scope.motion.personalNote) {
+ $scope.motion.personalNote.star = !$scope.motion.personalNote.star;
+ } else {
+ $scope.motion.personalNote = {star: true};
+ }
+ $http.put('/rest/motions/motion/' + $scope.motion.id + '/set_personal_note/',
+ $scope.motion.personalNote
+ );
+ };
// Inline editing functions
$scope.inlineEditing = MotionInlineEditing.createInstance($scope, motion,
@@ -1434,6 +1492,25 @@ angular.module('OpenSlidesApp.motions.site', [
}
);
$scope.commentsInlineEditing = MotionCommentsInlineEditing.createInstances($scope, motion);
+ $scope.personalNoteInlineEditing = MotionInlineEditing.createInstance($scope, motion,
+ 'personal-note-inline-editor', false,
+ function (obj) {
+ return motion.personalNote ? motion.personalNote.note : '';
+ },
+ function (obj) {
+ if (motion.personalNote) {
+ motion.personalNote.note = obj.editor.getData();
+ } else {
+ motion.personalNote = {note: obj.editor.getData()};
+ }
+ $http.put('/rest/motions/motion/' + $scope.motion.id + '/set_personal_note/',
+ motion.personalNote
+ );
+ obj.revert();
+ obj.disable();
+ return true; // Do not update the motion via patch request.
+ }
+ );
// Change recommendation creation functions
$scope.createChangeRecommendation = ChangeRecommmendationCreate;
diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html
index 3016bbe77..a04d3d7c2 100644
--- a/openslides/motions/static/templates/motions/motion-detail.html
+++ b/openslides/motions/static/templates/motions/motion-detail.html
@@ -59,7 +59,12 @@