diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js index e4be45c88..b63d0f5ff 100644 --- a/openslides/agenda/static/js/agenda/base.js +++ b/openslides/agenda/static/js/agenda/base.js @@ -295,8 +295,9 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) 'Assignment', // TODO: Remove this after refactoring of data loading on start. 'Topic', // TODO: Remove this after refactoring of data loading on start. 'Motion', // TODO: Remove this after refactoring of data loading on start. + 'MotionBlock', // TODO: Remove this after refactoring of data loading on start. 'Agenda', - function (Projector, Assignment, Topic, Motion, Agenda) { + function (Projector, Assignment, Topic, Motion, MotionBlock, Agenda) { return { getItem: function (projectorId) { var elementPromise; @@ -311,6 +312,13 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) }); }); break; + case 'motions/motion-block': + elementPromise = MotionBlock.find(element.id).then(function(motionBlock) { + return MotionBlock.loadRelations(motionBlock, 'agenda_item').then(function() { + return motionBlock.agenda_item; + }); + }); + break; case 'topics/topic': elementPromise = Topic.find(element.id).then(function(topic) { return Topic.loadRelations(topic, 'agenda_item').then(function() { diff --git a/openslides/core/signals.py b/openslides/core/signals.py index f2667ca21..bb08c9918 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -64,6 +64,10 @@ def create_builtin_projection_defaults(**kwargs): name='motions', display_name='Motions', projector=default_projector) + ProjectionDefault.objects.create( + name='motionBlocks', + display_name='Motion Blocks', + projector=default_projector) ProjectionDefault.objects.create( name='assignments', display_name='Elections', diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index 2ac7b4a2a..4f46c8b89 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -93,6 +93,25 @@ class CategoryAccessPermissions(BaseAccessPermissions): return CategorySerializer +class MotionBlockAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Category and CategoryViewSet. + """ + def check_permissions(self, user): + """ + Returns True if the user has read access model instances. + """ + return user.has_perm('motions.can_see') + + def get_serializer_class(self, user=None): + """ + Returns serializer class. + """ + from .serializers import MotionBlockSerializer + + return MotionBlockSerializer + + class WorkflowAccessPermissions(BaseAccessPermissions): """ Access permissions container for Workflow and WorkflowViewSet. diff --git a/openslides/motions/apps.py b/openslides/motions/apps.py index 413e12024..e82732e31 100644 --- a/openslides/motions/apps.py +++ b/openslides/motions/apps.py @@ -18,7 +18,7 @@ class MotionsAppConfig(AppConfig): from openslides.utils.rest_api import router from .config_variables import get_config_variables from .signals import create_builtin_workflows - from .views import CategoryViewSet, MotionViewSet, MotionPollViewSet, MotionChangeRecommendationViewSet, WorkflowViewSet + from .views import CategoryViewSet, MotionViewSet, MotionBlockViewSet, MotionPollViewSet, MotionChangeRecommendationViewSet, WorkflowViewSet # Define config variables config.update_config_variables(get_config_variables()) @@ -29,6 +29,7 @@ class MotionsAppConfig(AppConfig): # Register viewsets. router.register(self.get_model('Category').get_collection_string(), CategoryViewSet) router.register(self.get_model('Motion').get_collection_string(), MotionViewSet) + router.register(self.get_model('MotionBlock').get_collection_string(), MotionBlockViewSet) router.register(self.get_model('Workflow').get_collection_string(), WorkflowViewSet) router.register(self.get_model('MotionChangeRecommendation').get_collection_string(), MotionChangeRecommendationViewSet) diff --git a/openslides/motions/migrations/0005_auto_20161004_2350.py b/openslides/motions/migrations/0005_auto_20161004_2350.py new file mode 100644 index 000000000..6244fa0ab --- /dev/null +++ b/openslides/motions/migrations/0005_auto_20161004_2350.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-10-04 21:50 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + +import openslides.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('motions', '0004_auto_20160907_2343'), + ] + + operations = [ + migrations.CreateModel( + name='MotionBlock', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ], + options={ + 'default_permissions': (), + }, + bases=(openslides.utils.models.RESTModelMixin, models.Model), + ), + migrations.AddField( + model_name='motion', + name='motion_block', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.MotionBlock'), + ), + ] diff --git a/openslides/motions/models.py b/openslides/motions/models.py index f415fc01b..421786e31 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -23,6 +23,7 @@ from openslides.utils.search import user_name_helper from .access_permissions import ( CategoryAccessPermissions, MotionAccessPermissions, + MotionBlockAccessPermissions, MotionChangeRecommendationAccessPermissions, WorkflowAccessPermissions, ) @@ -116,6 +117,15 @@ class Motion(RESTModelMixin, models.Model): ForeignKey to one category of motions. """ + motion_block = models.ForeignKey( + 'MotionBlock', + on_delete=models.SET_NULL, + null=True, + blank=True) + """ + ForeignKey to one block of motions. + """ + origin = models.CharField(max_length=255, blank=True) """ A string to describe the origin of this motion e. g. that it was @@ -783,6 +793,66 @@ class Category(RESTModelMixin, models.Model): return self.name +class MotionBlockManager(models.Manager): + """ + Customized model manager to support our get_full_queryset method. + """ + def get_full_queryset(self): + """ + Returns the normal queryset with all motion blocks. In the + background the related agenda item is prefetched from the database. + """ + return self.get_queryset().prefetch_related('agenda_items') + + +class MotionBlock(RESTModelMixin, models.Model): + """ + Model for blocks of motions. + """ + access_permissions = MotionBlockAccessPermissions() + + objects = MotionBlockManager() + + title = models.CharField(max_length=255) + + # 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='topics') + + class Meta: + default_permissions = () + + def __str__(self): + return self.title + + @property + def agenda_item(self): + """ + Returns the related agenda item. + """ + # We support only one agenda item so just return the first element of + # the queryset. + return self.agenda_items.all()[0] + + @property + def agenda_item_id(self): + """ + Returns the id of the agenda item object related to this object. + """ + return self.agenda_item.pk + + def get_agenda_title(self): + return self.title + + def get_agenda_list_view_title(self): + return self.title + + @classmethod + def get_collection_string(cls): + # TODO: Fix generic method to support camelCase, #2480. + return 'motions/motion-block' + + class MotionLog(RESTModelMixin, models.Model): """Save a logmessage for a motion.""" diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index 862961662..78750b463 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -1,7 +1,7 @@ from ..core.exceptions import ProjectorException from ..utils.collection import CollectionElement from ..utils.projector import ProjectorElement -from .models import Motion +from .models import Motion, MotionBlock class MotionSlide(ProjectorElement): @@ -48,3 +48,44 @@ class MotionSlide(ProjectorElement): else: data = {'agenda_item_id': motion.agenda_item_id} return data + + +class MotionBlockSlide(ProjectorElement): + """ + Slide definitions for a block of motions (MotionBlock model). + """ + name = 'motions/motion-block' + + def check_data(self): + if not MotionBlock.objects.filter(pk=self.config_entry.get('id')).exists(): + raise ProjectorException('MotionBlock does not exist.') + + def get_requirements(self, config_entry): + try: + motion_block = MotionBlock.objects.get(pk=config_entry.get('id')) + except MotionBlock.DoesNotExist: + # MotionBlock does not exist. Just do nothing. + pass + else: + yield motion_block + yield motion_block.agenda_item + yield from motion_block.motion_set.all() + + def get_collection_elements_required_for_this(self, collection_element, config_entry): + output = super().get_collection_elements_required_for_this(collection_element, config_entry) + # Full update if a motion changes because then it may be appended to + # or removed from the block. + if collection_element.collection_string == Motion.get_collection_string(): + output.extend(self.get_requirements_as_collection_elements(config_entry)) + return output + + def update_data(self): + data = None + try: + motion_block = MotionBlock.objects.get(pk=self.config_entry.get('id')) + except MotionBlock.DoesNotExist: + # MotionBlock does not exist, so just do nothing. + pass + else: + data = {'agenda_item_id': motion_block.agenda_item_id} + return data diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index c094e3b3c..8a8373fce 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -16,6 +16,7 @@ from openslides.utils.rest_api import ( from .models import ( Category, Motion, + MotionBlock, MotionChangeRecommendation, MotionLog, MotionPoll, @@ -42,6 +43,15 @@ class CategorySerializer(ModelSerializer): fields = ('id', 'name', 'prefix',) +class MotionBlockSerializer(ModelSerializer): + """ + Serializer for motion.models.Category objects. + """ + class Meta: + model = MotionBlock + fields = ('id', 'title', 'agenda_item_id',) + + class StateSerializer(ModelSerializer): """ Serializer for motion.models.State objects. @@ -275,6 +285,7 @@ class MotionSerializer(ModelSerializer): 'active_version', 'parent', 'category', + 'motion_block', 'origin', 'submitters', 'supporters', @@ -300,6 +311,7 @@ class MotionSerializer(ModelSerializer): motion.reason = validated_data.get('reason', '') motion.identifier = validated_data.get('identifier') motion.category = validated_data.get('category') + motion.motion_block = validated_data.get('motion_block') motion.origin = validated_data.get('origin', '') motion.comments = validated_data.get('comments') motion.parent = validated_data.get('parent') @@ -319,8 +331,8 @@ class MotionSerializer(ModelSerializer): """ Customized method to update a motion. """ - # Identifier, category, origin and comments. - for key in ('identifier', 'category', 'origin', 'comments'): + # Identifier, category, motion_block, origin and comments. + for key in ('identifier', 'category', 'motion_block', 'origin', 'comments'): if key in validated_data.keys(): setattr(motion, key, validated_data[key]) diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index 55a3df574..3e2cdbe2f 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -3,10 +3,11 @@ "use strict"; angular.module('OpenSlidesApp.motions', [ - 'OpenSlidesApp.users', - 'OpenSlidesApp.motions.lineNumbering', - 'OpenSlidesApp.motions.diff', - 'OpenSlidesApp.motions.DOCX' + 'OpenSlidesApp.motions.motionBlock', + 'OpenSlidesApp.motions.lineNumbering', + 'OpenSlidesApp.motions.diff', + 'OpenSlidesApp.motions.DOCX', + 'OpenSlidesApp.users', ]) .factory('WorkflowState', [ @@ -115,7 +116,7 @@ angular.module('OpenSlidesApp.motions', [ if (type == 'yes' || type == 'no') { base = this.yes + this.no; } - } else if (config == "VALID" && type !== 'votescast' && type !== 'votesinvalid' && + } else if (config == "VALID" && type !== 'votescast' && type !== 'votesinvalid' && this.votesvalid > 0) { base = this.votesvalid; } else if (config == "CAST" && this.votescast > 0) { @@ -389,6 +390,10 @@ angular.module('OpenSlidesApp.motions', [ localField: 'category', localKey: 'category_id', }, + 'motions/motion-block': { + localField: 'motionBlock', + localKey: 'motion_block_id', + }, 'agenda/item': { localKey: 'agenda_item_id', localField: 'agenda_item', diff --git a/openslides/motions/static/js/motions/motion-block-projector.js b/openslides/motions/static/js/motions/motion-block-projector.js new file mode 100644 index 000000000..e1e791b0c --- /dev/null +++ b/openslides/motions/static/js/motions/motion-block-projector.js @@ -0,0 +1,32 @@ +(function () { + +'use strict'; + +angular.module('OpenSlidesApp.motions.motionBlockProjector', []) + + +// MotionBlock projector elements + +.config([ + 'slidesProvider', + function(slidesProvider) { + slidesProvider.registerSlide('motions/motion-block', { + template: 'static/templates/motions/slide_motion_block.html', + }); + } +]) + +.controller('SlideMotionBlockCtrl', [ + '$scope', + 'Motion', + 'MotionBlock', + function($scope, Motion, MotionBlock) { + // Attention! Each object that is used here has to be dealt on server side. + // Add it to the coresponding get_requirements method of the ProjectorElement + // class. + var id = $scope.element.id; + MotionBlock.bindOne(id, $scope, 'motionBlock'); + } +]); + +}()); diff --git a/openslides/motions/static/js/motions/motion-block.js b/openslides/motions/static/js/motions/motion-block.js new file mode 100644 index 000000000..9a88b0b23 --- /dev/null +++ b/openslides/motions/static/js/motions/motion-block.js @@ -0,0 +1,244 @@ +(function () { + +'use strict'; + +angular.module('OpenSlidesApp.motions.motionBlock', []) + + +// MotionBlock model + +.factory('MotionBlock', [ + 'DS', + 'jsDataModel', + 'gettext', + function(DS, jsDataModel, gettext) { + var name = 'motions/motion-block'; + return DS.defineResource({ + name: name, + useClass: jsDataModel, + verboseName: gettext('Motion block'), + methods: { + getResourceName: function () { + return name; + }, + getAgendaTitle: function () { + return this.title; + }, + }, + relations: { + belongsTo: { + 'agenda/item': { + localKey: 'agenda_item_id', + localField: 'agenda_item', + } + }, + hasMany: { + 'motions/motion': { + localField: 'motions', + foreignKey: 'motion_block_id', + } + }, + } + }); + } +]) + +.run(['MotionBlock', function(MotionBlock) {}]) + + +// MotionBlock views (list view, create dialog, update dialog) + +.factory('MotionBlockForm', [ + '$http', + 'gettextCatalog', + 'Agenda', + 'AgendaTree', + function ($http, gettextCatalog, Agenda, AgendaTree) { + return { + // Get ngDialog configuration. + getDialog: function (motionBlock) { + return { + template: 'static/templates/motions/motionBlock-form.html', + controller: (motionBlock) ? 'MotionBlockUpdateCtrl' : 'MotionBlockCreateCtrl', + className: 'ngdialog-theme-default wide-form', + closeByEscape: false, + closeByDocument: false, + resolve: { + motionBlock: function () { + return motionBlock; + } + } + }; + }, + // Get angular-formly fields. + getFormFields: function () { + return [ + { + key: 'title', + type: 'input', + templateOptions: { + label: gettextCatalog.getString('Title') + } + }, + { + key: 'showAsAgendaItem', + type: 'checkbox', + templateOptions: { + label: gettextCatalog.getString('Show as agenda item'), + description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.') + } + }, + { + key: 'agenda_parent_item_id', + type: 'select-single', + templateOptions: { + label: gettextCatalog.getString('Parent item'), + options: AgendaTree.getFlatTree(Agenda.getAll()), + ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id', + placeholder: gettextCatalog.getString('Select a parent item ...') + } + } + ]; + } + }; + } +]) + +// TODO: Rename this to MotionBlockListCtrl after $stateProvider is fixed, see #2479. +.controller('MotionblockListCtrl', [ + '$scope', + 'ngDialog', + 'MotionBlock', + 'MotionBlockForm', + function ($scope, ngDialog, MotionBlock, MotionBlockForm) { + // Two-way data binding for all MotionBlock instances. + MotionBlock.bindAll({}, $scope, 'motionBlocks'); + + // Dialog with a form to create or update a MotionBlock instance. + $scope.openFormDialog = function (motionBlock) { + ngDialog.open(MotionBlockForm.getDialog(motionBlock)); + }; + + // Confirm dialog to delete a MotionBlock instance. + $scope.delete = function (motionBlock) { + MotionBlock.destroy(motionBlock.id); + }; + + // TODO: In template we have filter toggleSort reverse sortColumn header. Use this stuff or remove it. + } +]) + +// TODO: Rename this to MotionBlockDetailCtrl after $stateProvider is fixed, see #2479. +.controller('MotionblockDetailCtrl', [ + '$scope', + 'ngDialog', + 'MotionBlockForm', + 'MotionBlock', + 'motionBlock', + 'Projector', + 'ProjectionDefault', + function($scope, ngDialog, MotionBlockForm, MotionBlock, motionBlock, Projector, ProjectionDefault) { + MotionBlock.bindOne(motionBlock.id, $scope, 'motionBlock'); + $scope.$watch(function () { + return Projector.lastModified(); + }, function () { + var projectiondefault = ProjectionDefault.filter({name: 'motionBlocks'})[0]; + if (projectiondefault) { + $scope.defaultProjectorId = projectiondefault.projector_id; + } + }); + $scope.openDialog = function (topic) { + ngDialog.open(MotionBlockForm.getDialog(motionBlock)); + }; + } +]) + +.controller('MotionBlockCreateCtrl', [ + '$scope', + 'MotionBlock', + 'MotionBlockForm', + 'AgendaUpdate', + function($scope, MotionBlock, MotionBlockForm, AgendaUpdate) { + // Prepare form. + $scope.model = {}; + $scope.model.showAsAgendaItem = true; + + // Get all form fields. + $scope.formFields = MotionBlockForm.getFormFields(); + + // Save form. + $scope.save = function (motionBlock) { + MotionBlock.create(motionBlock).then( + function (success) { + // type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item, + // see openslides.agenda.models.Item.ITEM_TYPE. + var changes = [{key: 'type', value: (motionBlock.showAsAgendaItem ? 1 : 2)}, + {key: 'parent_id', value: motionBlock.agenda_parent_item_id}]; + AgendaUpdate.saveChanges(success.agenda_item_id, changes); + $scope.closeThisDialog(); + }, + function (error) { + var message = ''; + for (var e in error.data) { + message += e + ': ' + error.data[e] + ' '; + } + $scope.alert = {type: 'danger', msg: message, show: true}; + } + ); + }; + } +]) + +.controller('MotionBlockUpdateCtrl', [ + '$scope', + '$state', + 'MotionBlock', + 'MotionBlockForm', + 'AgendaUpdate', + 'motionBlock', + function($scope, $state, MotionBlock, MotionBlockForm, AgendaUpdate, motionBlock) { + // TODO: Check #2486 and remove some agenda related code. + //MotionBlock.loadRelations(motionBlock, 'agenda_item'); + $scope.alert = {}; + + // Prepare form. Set initial values by creating a deep copy of + // motionBlock object so list/detail view is not updated while editing. + $scope.model = angular.copy(motionBlock); + + // Get all form fields. + $scope.formFields = MotionBlockForm.getFormFields(); + for (var i = 0; i < $scope.formFields.length; i++) { + if ($scope.formFields[i].key == 'showAsAgendaItem') { + // Get state from agenda item (hidden/internal or agenda item). + $scope.formFields[i].defaultValue = !motionBlock.agenda_item.is_hidden; + } else if ($scope.formFields[i].key == 'agenda_parent_item_id') { + $scope.formFields[i].defaultValue = motionBlock.agenda_item.parent_id; + } + } + // Save form. + $scope.save = function (motionBlock) { + MotionBlock.create(motionBlock).then( + function (success) { + // type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item, + // see openslides.agenda.models.Item.ITEM_TYPE. + var changes = [{key: 'type', value: (motionBlock.showAsAgendaItem ? 1 : 2)}, + {key: 'parent_id', value: motionBlock.agenda_parent_item_id}]; + AgendaUpdate.saveChanges(success.agenda_item_id,changes); + $scope.closeThisDialog(); + }, + function (error) { + // Save error: revert all changes by restore + // (refresh) original motionBlock object from server + MotionBlock.refresh(motionBlock); // TODO: Why do we need a refresh here? + var message = ''; + for (var e in error.data) { + message += e + ': ' + error.data[e] + ' '; + } + $scope.alert = {type: 'danger', msg: message, show: true}; + } + ); + }; + } +]); + +}()); diff --git a/openslides/motions/static/js/motions/projector.js b/openslides/motions/static/js/motions/projector.js index fa315b458..b5054dc8e 100644 --- a/openslides/motions/static/js/motions/projector.js +++ b/openslides/motions/static/js/motions/projector.js @@ -2,7 +2,10 @@ 'use strict'; -angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions']) +angular.module('OpenSlidesApp.motions.projector', [ + 'OpenSlidesApp.motions', + 'OpenSlidesApp.motions.motionBlockProjector', +]) .config([ 'slidesProvider', diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index 44aacb6a8..9626161e0 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -4,7 +4,6 @@ angular.module('OpenSlidesApp.motions.site', [ 'OpenSlidesApp.motions', - 'OpenSlidesApp.motions.diff', 'OpenSlidesApp.motions.motionservices', 'OpenSlidesApp.poll.majority', 'OpenSlidesApp.core.pdf', @@ -50,6 +49,9 @@ angular.module('OpenSlidesApp.motions.site', [ categories: function(Category) { return Category.findAll(); }, + motionBlocks: function(MotionBlock) { + return MotionBlock.findAll(); + }, tags: function(Tag) { return Tag.findAll(); }, @@ -81,6 +83,9 @@ angular.module('OpenSlidesApp.motions.site', [ categories: function(Category) { return Category.findAll(); }, + motionBlocks: function(MotionBlock) { + return MotionBlock.findAll(); + }, users: function(User) { return User.findAll().catch( function () { @@ -198,6 +203,66 @@ angular.module('OpenSlidesApp.motions.site', [ }, controller: 'CategorySortCtrl', templateUrl: 'static/templates/motions/category-sort.html' + }) + // MotionBlock + .state('motions.motionBlock', { + url: '/blocks', + abstract: true, + template: '', + }) + .state('motions.motionBlock.list', { + resolve: { + motionBlocks: function (MotionBlock) { + return MotionBlock.findAll(); + }, + items: function(Agenda) { + return Agenda.findAll().catch( + function () { + return null; + } + ); + } + } + }) + .state('motions.motionBlock.detail', { + resolve: { + motionBlock: function(MotionBlock, $stateParams) { + return MotionBlock.find($stateParams.id); + }, + items: function(Agenda) { + return Agenda.findAll().catch( + function () { + return null; + } + ); + } + } + }) + // redirects to motionBlock detail and opens motionBlock edit form dialog, uses edit url, + // used by ui-sref links from agenda only + // (from motionBlock controller use MotionBlockForm factory instead to open dialog in front + // of current view without redirect) + .state('motions.motionBlock.detail.update', { + onEnter: ['$stateParams', '$state', 'ngDialog', 'MotionBlock', + function($stateParams, $state, ngDialog, MotionBlock) { + ngDialog.open({ + template: 'static/templates/motions/motionBlock-form.html', + controller: 'MotionBlockUpdateCtrl', + className: 'ngdialog-theme-default wide-form', + closeByEscape: false, + closeByDocument: false, + resolve: { + motionBlock: function () { + return motionBlock; + } + }, + preCloseCallback: function() { + $state.go('motions.motionBlock.detail', {motionBlock: $stateParams.id}); + return true; + } + }); + } + ], }); } ]) @@ -306,12 +371,13 @@ angular.module('OpenSlidesApp.motions.site', [ 'Category', 'Config', 'Mediafile', + 'MotionBlock', 'Tag', 'User', 'Workflow', 'Agenda', 'AgendaTree', - function (gettextCatalog, operator, Editor, MotionComment, Category, Config, Mediafile, Tag, User, Workflow, Agenda, AgendaTree) { + function (gettextCatalog, operator, Editor, MotionComment, Category, Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree) { return { // ngDialog for motion form getDialog: function (motion) { @@ -453,6 +519,17 @@ angular.module('OpenSlidesApp.motions.site', [ }, hideExpression: '!model.more' }, + { + key: 'motion_block_id', + type: 'select-single', + templateOptions: { + label: gettextCatalog.getString('Motion block'), + options: MotionBlock.getAll(), + ngOptions: 'option.id as option.title for option in to.options', + placeholder: gettextCatalog.getString('Select or search a motion block ...') + }, + hideExpression: '!model.more' + }, { key: 'origin', type: 'input', diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html index e622df746..bf2e41f68 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -159,6 +159,11 @@

Origin

{{ motion.origin }} + + +

Motion block

+ {{ motion.motionBlock }} +

Voting result

diff --git a/openslides/motions/static/templates/motions/motion-list.html b/openslides/motions/static/templates/motions/motion-list.html index ed4e64e12..1265e05f1 100644 --- a/openslides/motions/static/templates/motions/motion-list.html +++ b/openslides/motions/static/templates/motions/motion-list.html @@ -9,6 +9,10 @@ Categories + + + Motion blocks + Tags diff --git a/openslides/motions/static/templates/motions/motionBlock-detail.html b/openslides/motions/static/templates/motions/motionBlock-detail.html new file mode 100644 index 000000000..319be6e89 --- /dev/null +++ b/openslides/motions/static/templates/motions/motionBlock-detail.html @@ -0,0 +1,30 @@ +
+
+ +

{{ motionBlock.agenda_item.getTitle() }}

+

Motion Block

+
+
+ +
+ {{ motionBlock.motions }} +
diff --git a/openslides/motions/static/templates/motions/motionBlock-form.html b/openslides/motions/static/templates/motions/motionBlock-form.html new file mode 100644 index 000000000..e28d0df3c --- /dev/null +++ b/openslides/motions/static/templates/motions/motionBlock-form.html @@ -0,0 +1,17 @@ +

Edit motion block

+

New motion block

+ +
+ {{ alert.msg }} +
+ +
+ + + + +
diff --git a/openslides/motions/static/templates/motions/motionBlock-list.html b/openslides/motions/static/templates/motions/motionBlock-list.html new file mode 100644 index 000000000..966485938 --- /dev/null +++ b/openslides/motions/static/templates/motions/motionBlock-list.html @@ -0,0 +1,49 @@ +
+
+ +

Motion blocks

+
+
+ +
+
+
+ +
+
+ + + + + + +
+ Name + + +
+ + {{ motionBlock.title }} + +
+ + Edit | + + Delete +
+
+
diff --git a/openslides/motions/static/templates/motions/slide_motion_block.html b/openslides/motions/static/templates/motions/slide_motion_block.html new file mode 100644 index 000000000..13982cc3e --- /dev/null +++ b/openslides/motions/static/templates/motions/slide_motion_block.html @@ -0,0 +1,4 @@ +
+

{{ motionBlock.agenda_item.getTitle() }}

+
{{ motion.identifier }}
+
diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 4af58bd2b..b73835c06 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -24,6 +24,7 @@ from openslides.utils.views import APIView, PDFView, SingleObjectMixin from .access_permissions import ( CategoryAccessPermissions, MotionAccessPermissions, + MotionBlockAccessPermissions, MotionChangeRecommendationAccessPermissions, WorkflowAccessPermissions, ) @@ -31,6 +32,7 @@ from .exceptions import WorkflowError from .models import ( Category, Motion, + MotionBlock, MotionChangeRecommendation, MotionPoll, MotionVersion, @@ -89,6 +91,8 @@ class MotionViewSet(ModelViewSet): # Non-staff users are not allowed to send submitter or supporter data. self.permission_denied(request) + # TODO: Should non staff users be allowed to set motions to blocks or send categories, ...? #2506 + # Check permission to send comment data. if not request.user.has_perm('motions.can_see_and_manage_comments'): try: @@ -373,7 +377,7 @@ class CategoryViewSet(ModelViewSet): API endpoint for categories. There are the following views: metadata, list, retrieve, create, - partial_update, update and destroy. + partial_update, update, destroy and numbering. """ access_permissions = CategoryAccessPermissions() queryset = Category.objects.all() @@ -445,6 +449,32 @@ class CategoryViewSet(ModelViewSet): return response +class MotionBlockViewSet(ModelViewSet): + """ + API endpoint for motion blocks. + + There are the following views: metadata, list, retrieve, create, + partial_update, update and destroy. + """ + access_permissions = MotionBlockAccessPermissions() + queryset = MotionBlock.objects.all() + + def check_view_permissions(self): + """ + Returns True if the user has required permissions. + """ + if self.action in ('list', 'retrieve'): + result = self.get_access_permissions().check_permissions(self.request.user) + elif self.action == 'metadata': + result = self.request.user.has_perm('motions.can_see') + elif self.action in ('create', 'partial_update', 'update', 'destroy'): + result = (self.request.user.has_perm('motions.can_see') and + self.request.user.has_perm('motions.can_manage')) + else: + result = False + return result + + class WorkflowViewSet(ModelViewSet): """ API endpoint for workflows.