diff --git a/.travis.yml b/.travis.yml index 9a9f8ec19..24f2819b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ script: - node_modules/.bin/karma start --browsers PhantomJS tests/karma/karma.conf.js - DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.unit - - coverage report --fail-under=43 + - coverage report --fail-under=42 - DJANGO_SETTINGS_MODULE='tests.settings' coverage run ./manage.py test tests.integration - coverage report --fail-under=73 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dff0b5cde..9d25be287 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,7 +8,8 @@ Version 2.3 (unreleased) ======================== Motions: -- New feature to scroll the projector to a specific line [#3748]. + - New feature to scroll the projector to a specific line [#3748]. + - New possibility to sort submitters [#3647]. Version 2.2 (2018-06-06) diff --git a/openslides/agenda/static/templates/agenda/list-of-speakers-partial-management.html b/openslides/agenda/static/templates/agenda/list-of-speakers-partial-management.html index 8effcedcd..5299fdea2 100644 --- a/openslides/agenda/static/templates/agenda/list-of-speakers-partial-management.html +++ b/openslides/agenda/static/templates/agenda/list-of-speakers-partial-management.html @@ -1,26 +1,26 @@
-
-
- - - - - - + + + + - + + +
-

@@ -79,7 +79,7 @@

    -
  1. +
  2. {{ $index + 1 }}. {{ speaker.user.get_full_name() }} diff --git a/openslides/core/static/css/core/_site.scss b/openslides/core/static/css/core/_site.scss index 124eb6d17..63c12c2e8 100644 --- a/openslides/core/static/css/core/_site.scss +++ b/openslides/core/static/css/core/_site.scss @@ -274,11 +274,8 @@ strong, b, th { .meta { - h3 { - font-family: $font-condensed-light; - } - .heading, h3 { + font-family: $font-condensed-light; font-size: 22px; line-height: 24px; font-weight: 300; diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index 013f8ab9f..57ccba291 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -43,7 +43,8 @@ class MotionAccessPermissions(BaseAccessPermissions): for full in full_data: # Check if user is submitter of this motion. if isinstance(user, CollectionElement): - is_submitter = user.get_full_data()['id'] in full.get('submitters_id', []) + is_submitter = user.get_full_data()['id'] in [ + submitter['user_id'] for submitter in full.get('submitters', [])] else: # Anonymous users can not be submitters. is_submitter = False diff --git a/openslides/motions/migrations/0006_submitter_model.py b/openslides/motions/migrations/0006_submitter_model.py new file mode 100644 index 000000000..dc890b5d2 --- /dev/null +++ b/openslides/motions/migrations/0006_submitter_model.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2018-02-09 07:18 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from django.db import migrations, models + +import openslides.utils.models + + +def move_submitters_to_own_model(apps, schema_editor): + Motion = apps.get_model('motions', 'Motion') + Submitter = apps.get_model('motions', 'Submitter') + + for motion in Motion.objects.all(): + weight = 0 + for user in motion.submittersOld.all(): + # We cannot use the add method here, so do it manually... + if Submitter.objects.filter(user=user, motion=motion).exists(): + continue # The user is already a submitter. Skip this duplicate. + if isinstance(user, AnonymousUser): + continue # Skip the anonymous + + submitter = Submitter(user=user, motion=motion, weight=weight) + submitter.save(force_insert=True) + weight += 1 + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('motions', '0005_auto_20180202_1318'), + ] + + operations = [ + migrations.RenameField( + model_name='motion', + old_name='submitters', + new_name='submittersOld', + ), + migrations.CreateModel( + name='Submitter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('weight', models.IntegerField(null=True)), + ], + options={ + 'default_permissions': (), + }, + bases=(openslides.utils.models.RESTModelMixin, models.Model), + ), + migrations.AddField( + model_name='submitter', + name='motion', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submitters', to='motions.Motion'), + ), + migrations.AddField( + model_name='submitter', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.RunPython( + move_submitters_to_own_model + ), + migrations.RemoveField( + model_name='motion', + name='submittersOld', + ), + ] diff --git a/openslides/motions/models.py b/openslides/motions/models.py index 0f5fc73c0..afca64661 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -1,6 +1,7 @@ from typing import Any, Dict # noqa from django.conf import settings +from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db import IntegrityError, models, transaction @@ -21,6 +22,7 @@ from openslides.poll.models import ( CollectDefaultVotesMixin, ) from openslides.utils.autoupdate import inform_changed_data +from openslides.utils.exceptions import OpenSlidesError from openslides.utils.models import RESTModelMixin from .access_permissions import ( @@ -157,11 +159,6 @@ class Motion(RESTModelMixin, models.Model): Tags to categorise motions. """ - submitters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_submitters', blank=True) - """ - Users who submit this motion. - """ - supporters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_supporters', blank=True) """ Users who support this motion. @@ -548,7 +545,7 @@ class Motion(RESTModelMixin, models.Model): """ Returns True if user is a submitter of this motion, else False. """ - return user in self.submitters.all() + return self.submitters.filter(user=user).exists() def is_supporter(self, user): """ @@ -706,6 +703,69 @@ class Motion(RESTModelMixin, models.Model): yield from amendment.get_amendments_deep() +class SubmitterManager(models.Manager): + """ + Manager for Submitter model. Provides a customized add method. + """ + def add(self, user, motion, skip_autoupdate=False): + """ + Customized manager method to prevent anonymous users to be a + submitter and that someone is not twice a submitter. Cares also + for the initial sorting of the submitters. + """ + if self.filter(user=user, motion=motion).exists(): + raise OpenSlidesError( + _('{user} is already a submitter.').format(user=user)) + if isinstance(user, AnonymousUser): + raise OpenSlidesError( + _('An anonymous user can not be a submitter.')) + weight = (self.filter(motion=motion).aggregate( + models.Max('weight'))['weight__max'] or 0) + submitter = self.model(user=user, motion=motion, weight=weight + 1) + submitter.save(force_insert=True, skip_autoupdate=skip_autoupdate) + return submitter + + +class Submitter(RESTModelMixin, models.Model): + """ + M2M Model for submitters. + """ + + objects = SubmitterManager() + """ + Use custom Manager. + """ + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE) + """ + ForeignKey to the user who is the submitter. + """ + + motion = models.ForeignKey( + Motion, + on_delete=models.CASCADE, + related_name='submitters') + """ + ForeignKey to the motion. + """ + + weight = models.IntegerField(null=True) + + class Meta: + default_permissions = () + + def __str__(self): + return str(self.user) + + def get_root_rest_element(self): + """ + Returns the motion to this instance which is the root REST element. + """ + return self.motion + + class MotionVersion(RESTModelMixin, models.Model): """ A MotionVersion object saves some date of the motion. diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index bbc02575f..68d9e9073 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -29,7 +29,8 @@ class MotionSlide(ProjectorElement): yield motion.agenda_item yield motion.state.workflow yield from self.required_motions_for_state_and_recommendation(motion) - yield from motion.submitters.all() + for submitter in motion.submitters.all(): + yield submitter.user yield from motion.supporters.all() yield from MotionChangeRecommendation.objects.filter(motion_version=motion.get_active_version().id) diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index 0dcdd4031..da929765b 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -24,6 +24,7 @@ from .models import ( MotionPoll, MotionVersion, State, + Submitter, Workflow, ) @@ -290,6 +291,20 @@ class MotionChangeRecommendationSerializer(ModelSerializer): return data +class SubmitterSerializer(ModelSerializer): + """ + Serializer for motion.models.Submitter objects. + """ + class Meta: + model = Submitter + fields = ( + 'id', + 'user', + 'motion', + 'weight', + ) + + class MotionSerializer(ModelSerializer): """ Serializer for motion.models.Motion objects. @@ -310,6 +325,7 @@ class MotionSerializer(ModelSerializer): write_only=True) agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=2) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) + submitters = SubmitterSerializer(many=True, read_only=True) class Meta: model = Motion @@ -374,10 +390,6 @@ class MotionSerializer(ModelSerializer): motion.agenda_item_update_information['type'] = validated_data.get('agenda_type') motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id') motion.save() - if validated_data.get('submitters'): - motion.submitters.add(*validated_data['submitters']) - elif validated_data['request_user'].is_authenticated(): - motion.submitters.add(validated_data['request_user']) motion.supporters.add(*validated_data.get('supporters', [])) motion.attachments.add(*validated_data.get('attachments', [])) motion.tags.add(*validated_data.get('tags', [])) diff --git a/openslides/motions/signals.py b/openslides/motions/signals.py index 79303177a..4bc838522 100644 --- a/openslides/motions/signals.py +++ b/openslides/motions/signals.py @@ -130,6 +130,7 @@ def required_users(sender, request_user, **kwargs): if has_perm(request_user, 'motions.can_see'): for motion_collection_element in Collection(Motion.get_collection_string()).element_generator(): full_data = motion_collection_element.get_full_data() - submitters_supporters.update(full_data['submitters_id']) + submitters_supporters.update( + [submitter['user_id'] for submitter in full_data['submitters']]) submitters_supporters.update(full_data['supporters_id']) return submitters_supporters diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index 3f868e113..93de52069 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -209,6 +209,22 @@ angular.module('OpenSlidesApp.motions', [ } ]) +.factory('Submitter', [ + 'DS', + function (DS) { + return DS.defineResource({ + name: 'motions/submitter', + relations: { + belongsTo: { + 'users/user': { + localField: 'user', + localKey: 'user_id', + } + } + } + }); + } +]) .factory('Motion', [ 'DS', @@ -571,6 +587,7 @@ angular.module('OpenSlidesApp.motions', [ * There are the following possible actions. * - see * - update + * - update_submitters * - delete * - create_poll * - support @@ -604,6 +621,8 @@ angular.module('OpenSlidesApp.motions', [ this.state.allow_submitter_edit ) ); + case 'update_submitters': + return operator.hasPerms('motions.can_manage'); case 'delete': return ( operator.hasPerms('motions.can_manage') || @@ -755,20 +774,18 @@ angular.module('OpenSlidesApp.motions', [ localField: 'attachments', localKeys: 'attachments_id', }, - 'users/user': [ - { - localField: 'submitters', - localKeys: 'submitters_id', - }, - { - localField: 'supporters', - localKeys: 'supporters_id', - } - ], + 'users/user': { + localField: 'supporters', + localKeys: 'supporters_id', + }, 'motions/motion-poll': { localField: 'polls', foreignKey: 'motion_id', - } + }, + 'motions/submitter': { + localField: 'submitters', + foreignKey: 'motion_id', + }, }, hasOne: { 'motions/workflowstate': [ @@ -987,7 +1004,8 @@ angular.module('OpenSlidesApp.motions', [ 'Category', 'Workflow', 'MotionChangeRecommendation', - function(Motion, Category, Workflow, MotionChangeRecommendation) {} + 'Submitter', + function(Motion, Category, Workflow, MotionChangeRecommendation, Submitter) {} ]) diff --git a/openslides/motions/static/js/motions/csv.js b/openslides/motions/static/js/motions/csv.js index c839498ae..756552864 100644 --- a/openslides/motions/static/js/motions/csv.js +++ b/openslides/motions/static/js/motions/csv.js @@ -5,11 +5,12 @@ angular.module('OpenSlidesApp.motions.csv', []) .factory('MotionCsvExport', [ + '$filter', 'gettextCatalog', 'Config', 'CsvDownload', 'lineNumberingService', - function (gettextCatalog, Config, CsvDownload, lineNumberingService) { + function ($filter, gettextCatalog, Config, CsvDownload, lineNumberingService) { var makeHeaderline = function (params) { var headerline = ['Identifier', 'Title']; if (params.include.text) { @@ -77,8 +78,12 @@ angular.module('OpenSlidesApp.motions.csv', []) // Submitters if (params.include.submitters) { var submitters = []; - angular.forEach(motion.submitters, function(user) { - var user_short_name = [user.title, user.first_name, user.last_name].join(' ').trim(); + _.forEach($filter('orderBy')(motion.submitters, 'weight'), function (user) { + var user_short_name = [ + user.user.title, + user.user.first_name, + user.user.last_name + ].join(' ').trim(); submitters.push(user_short_name); }); row.push('"' + submitters.join('; ') + '"'); diff --git a/openslides/motions/static/js/motions/docx.js b/openslides/motions/static/js/motions/docx.js index 7d0baa8e4..eeb66a263 100644 --- a/openslides/motions/static/js/motions/docx.js +++ b/openslides/motions/static/js/motions/docx.js @@ -7,6 +7,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx']) .factory('MotionDocxExport', [ '$http', '$q', + '$filter', 'operator', 'Config', 'Category', @@ -15,8 +16,8 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx']) 'lineNumberingService', 'Html2DocxConverter', 'MotionComment', - function ($http, $q, operator, Config, Category, gettextCatalog, FileSaver, lineNumberingService, - Html2DocxConverter, MotionComment) { + function ($http, $q, $filter, operator, Config, Category, gettextCatalog, + FileSaver, lineNumberingService, Html2DocxConverter, MotionComment) { var PAGEBREAK = ''; @@ -116,9 +117,11 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx']) id: motion.id, identifier: motion.identifier || '', title: title, - submitters: params.include.submitters ? _.map(motion.submitters, function (submitter) { - return submitter.get_full_name(); - }).join(', ') : '', + submitters: params.include.submitters ? _.map( + $filter('orderBy')(motion.submitters, 'weight'), function (submitter) { + return submitter.user.get_full_name(); + } + ).join(', ') : '', status: motion.getStateName(), // Miscellaneous stuff preamble: gettextCatalog.getString(Config.get('motions_preamble').value), diff --git a/openslides/motions/static/js/motions/pdf.js b/openslides/motions/static/js/motions/pdf.js index 1b51fb797..4364272e7 100644 --- a/openslides/motions/static/js/motions/pdf.js +++ b/openslides/motions/static/js/motions/pdf.js @@ -6,6 +6,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) .factory('MotionContentProvider', [ '$q', + '$filter', 'operator', 'gettextCatalog', 'PDFLayout', @@ -17,7 +18,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) 'Motion', 'MotionComment', 'OpenSlidesSettings', - function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, + function($q, $filter, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, HTMLValidizer, Category, Config, Motion, MotionComment, OpenSlidesSettings) { /** * Provides the content as JS objects for Motions in pdfMake context @@ -86,9 +87,11 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) var metaTableBody = []; // submitters - var submitters = _.map(motion.submitters, function (submitter) { - return submitter.get_full_name(); - }).join(', '); + var submitters = _.map( + $filter('orderBy')(motion.submitters, 'weight'), function (submitter) { + return submitter.user.get_full_name(); + } + ).join(', '); if (params.include.submitters) { metaTableBody.push([ { diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index e04173578..14dc9786c 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -89,6 +89,19 @@ angular.module('OpenSlidesApp.motions.site', [ } ] }) + .state('motions.motion.submitters', { + url: '/submitters/{id:int}', + controller: 'MotionSubmitterCtrl', + resolve: { + motionId: ['$stateParams', function($stateParams) { + return $stateParams.id; + }], + }, + data: { + title: gettext('Submitters'), + basePerm: 'motions.can_manage', + }, + }) .state('motions.motion.import', { url: '/import', controller: 'MotionImportCtrl', @@ -388,67 +401,73 @@ angular.module('OpenSlidesApp.motions.site', [ getFormFields: function (isCreateForm) { var workflows = Workflow.getAll(); var images = Mediafile.getAllImages(); - var formFields = [ - { + var formFields = []; + formFields.push({ key: 'identifier', type: 'input', templateOptions: { label: gettextCatalog.getString('Identifier') }, hide: true - }, - { - key: 'submitters_id', - type: 'select-multiple', - templateOptions: { - label: gettextCatalog.getString('Submitters'), - options: User.getAll(), - ngOptions: 'option.id as option.full_name for option in to.options', - placeholder: gettextCatalog.getString('Select or search a submitter ...') + }); + + if (isCreateForm) { + formFields.push({ + key: 'submitters_id', + type: 'select-multiple', + templateOptions: { + label: gettextCatalog.getString('Submitters'), + options: User.getAll(), + ngOptions: 'option.id as option.full_name for option in to.options', + placeholder: gettextCatalog.getString('Select or search a submitter ...'), + }, + hide: !operator.hasPerms('motions.can_manage') + }); + } + + formFields = formFields.concat([ + { + key: 'title', + type: 'input', + templateOptions: { + label: gettextCatalog.getString('Title'), + required: true + } }, - hide: !operator.hasPerms('motions.can_manage') - }, - { - key: 'title', - type: 'input', - templateOptions: { - label: gettextCatalog.getString('Title'), - required: true + { + template: '

    ' + Config.translate(Config.get('motions_preamble').value) + '

    ' + }, + { + key: 'text', + type: 'editor', + templateOptions: { + label: gettextCatalog.getString('Text'), + required: true + }, + data: { + ckeditorOptions: Editor.getOptions() + } + }, + { + key: 'reason', + type: 'editor', + templateOptions: { + label: gettextCatalog.getString('Reason'), + }, + data: { + ckeditorOptions: Editor.getOptions() + } + }, + { + key: 'disable_versioning', + type: 'checkbox', + templateOptions: { + label: gettextCatalog.getString('Trivial change'), + description: gettextCatalog.getString("Don't create a new version.") + }, + hide: true } - }, - { - template: '

    ' + Config.translate(Config.get('motions_preamble').value) + '

    ' - }, - { - key: 'text', - type: 'editor', - templateOptions: { - label: gettextCatalog.getString('Text'), - required: true - }, - data: { - ckeditorOptions: Editor.getOptions() - } - }, - { - key: 'reason', - type: 'editor', - templateOptions: { - label: gettextCatalog.getString('Reason'), - }, - data: { - ckeditorOptions: Editor.getOptions() - } - }, - { - key: 'disable_versioning', - type: 'checkbox', - templateOptions: { - label: gettextCatalog.getString('Trivial change'), - description: gettextCatalog.getString("Don't create a new version.") - }, - hide: true - }]; + ]); // show as agenda item + parent item if (isCreateForm) { @@ -1192,7 +1211,7 @@ angular.module('OpenSlidesApp.motions.site', [ ]; $scope.filter.propertyDict = { 'submitters': function (submitter) { - return submitter.get_short_name(); + return submitter.user.get_short_name(); }, 'supporters': function (supporter) { return supporter.get_short_name(); @@ -1235,7 +1254,7 @@ angular.module('OpenSlidesApp.motions.site', [ display_name: gettext('Identifier')}, {name: 'getTitle()', display_name: gettext('Title')}, - {name: 'submitters[0].get_short_name()', + {name: 'submitters[0].user.get_short_name()', display_name: gettext('Submitters')}, {name: 'category.' + Config.get('motions_export_category_sorting').value, display_name: gettext('Category')}, @@ -2117,7 +2136,7 @@ angular.module('OpenSlidesApp.motions.site', [ 'operator', 'ErrorMessage', 'EditingWarning', - function($scope, $state, Motion, Category, Config, Mediafile, MotionForm, + function ($scope, $state, Motion, Category, Config, Mediafile, MotionForm, Tag, User, Workflow, Agenda, motionId, operator, ErrorMessage, EditingWarning) { Category.bindAll({}, $scope, 'categories'); @@ -2176,7 +2195,7 @@ angular.module('OpenSlidesApp.motions.site', [ Motion.inject(motion); // save changed motion object on server Motion.save(motion).then( - function(success) { + function (success) { if (gotoDetailView) { $state.go('motions.motion.detail', {id: success.id}); } @@ -2201,7 +2220,7 @@ angular.module('OpenSlidesApp.motions.site', [ 'motionpollId', 'voteNumber', 'ErrorMessage', - function($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId, + function ($scope, gettextCatalog, MotionPoll, MotionPollForm, motionpollId, voteNumber, ErrorMessage) { // set initial values for form model by create deep copy of motionpoll object // so detail view is not updated while editing poll @@ -2220,16 +2239,76 @@ angular.module('OpenSlidesApp.motions.site', [ votesinvalid: poll.votesinvalid, votescast: poll.votescast }) - .then(function(success) { + .then(function (success) { $scope.alert.show = false; $scope.closeThisDialog(); - }, function(error) { + }, function (error) { $scope.alert = ErrorMessage.forAlert(error); }); }; } ]) +.controller('MotionSubmitterCtrl', [ + '$scope', + '$filter', + '$http', + 'User', + 'Motion', + 'motionId', + 'ErrorMessage', + function ($scope, $filter, $http, User, Motion, motionId, ErrorMessage) { + User.bindAll({}, $scope, 'users'); + $scope.submitterSelectBox = {}; + $scope.alert = {}; + + $scope.$watch(function () { + return Motion.lastModified(motionId); + }, function () { + $scope.motion = Motion.get(motionId); + $scope.submitters = $filter('orderBy')($scope.motion.submitters, 'weight'); + }); + + $scope.addSubmitter = function (userId) { + $scope.submitterSelectBox = {}; + $http.post('/rest/motions/motion/' + $scope.motion.id + '/manage_submitters/', { + 'user': userId + }).then( + function (success) { + $scope.alert.show = false; + }, function (error) { + $scope.alert = ErrorMessage.forAlert(error); + } + ); + }; + + $scope.removeSubmitter = function (userId) { + $http.delete('/rest/motions/motion/' + $scope.motion.id + '/manage_submitters/', { + headers: {'Content-Type': 'application/json'}, + data: JSON.stringify({user: userId}) + }).then( + function (success) { + $scope.alert.show = false; + }, function (error) { + $scope.alert = ErrorMessage.forAlert(error); + } + ); + }; + + // save reordered list of submitters + $scope.treeOptions = { + dropped: function (event) { + var submitterIds = _.map($scope.submitters, function (submitter) { + return submitter.id; + }); + $http.post('/rest/motions/motion/' + $scope.motion.id + '/sort_submitters/', { + submitters: submitterIds, + }); + } + }; + } +]) + .controller('MotionImportCtrl', [ '$scope', '$q', @@ -2239,7 +2318,7 @@ angular.module('OpenSlidesApp.motions.site', [ 'MotionBlock', 'User', 'MotionCsvExport', - function($scope, $q, gettext, Category, Motion, MotionBlock, User, MotionCsvExport) { + function ($scope, $q, gettext, Category, Motion, MotionBlock, User, MotionCsvExport) { // set initial data for csv import $scope.motions = []; @@ -2295,7 +2374,7 @@ angular.module('OpenSlidesApp.motions.site', [ } // submitter if (motion.submitter && motion.submitter !== '') { - angular.forEach(User.getAll(), function (user) { + _.forEach(User.getAll(), function (user) { var user_short_name = [user.title, user.first_name, user.last_name].join(' ').trim(); if (user_short_name == motion.submitter.trim()) { motion.submitters_id = [user.id]; diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html index f365ffa30..fab9b04e2 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -125,9 +125,15 @@
    -

    Submitters

    -
    - {{ submitter.get_full_name() }} +

    Submitters

    +

    + Submitters + + + +

    +
    + {{ submitter.user.get_full_name() }}
    @@ -211,7 +217,7 @@
    - {{ config('motions_recommendations_by') }} + {{ config('motions_recommendations_by') | translate }}