diff --git a/CHANGELOG b/CHANGELOG
index e7bb8e99a..a32c10cef 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -32,6 +32,9 @@ Motions:
- Changed label of former state "commited a bill" to "refered to committee".
- Added options to calculate percentages on different bases.
- Added majority calculation.
+- Added blocks for motions which can set as agenda item.
+ Set states for multiple motions of a motion block by following
+ the recommendations of each motion.
Users:
- Added field is_committee and new default group Committees.
diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js
index a399a6afd..132f01b7a 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/core/static/js/core/site.js b/openslides/core/static/js/core/site.js
index 20274ee22..14a2e1191 100644
--- a/openslides/core/static/js/core/site.js
+++ b/openslides/core/static/js/core/site.js
@@ -165,10 +165,8 @@ angular.module('OpenSlidesApp.core.site', [
}
angular.forEach(views, function(config, name) {
-
- // Sets default values for templateUrl
- var patterns = state.name.split('.'),
- templateUrl,
+ // Sets additional default values for templateUrl
+ var templateUrl,
controller,
defaultControllers = {
create: 'CreateCtrl',
@@ -177,22 +175,43 @@ angular.module('OpenSlidesApp.core.site', [
detail: 'DetailCtrl',
};
- // templateUrl
- if (_.last(patterns).match(/(create|update)/)) {
- // When state_patterns is in the form "app.module.create" or
- // "app.module.update", use the form template.
- templateUrl = 'static/templates/' + patterns[0] + '/' + patterns[1] + '-form.html';
- } else {
- // Replaces the first point through a slash (the app name)
- var appName = state.name.replace('.', '/');
- // Replaces any folowing points though a -
- templateUrl = 'static/templates/' + appName.replace(/\./g, '-') + '.html';
+ // Split up state name
+ // example: "motions.motion.detail.update" -> ['motions', 'motion', 'detail', 'update']
+ var patterns = state.name.split('.');
+
+ // set app and module name from state
+ // - appName: patterns[0] (e.g. "motions")
+ // - moduleNames: patterns without first element (e.g. ["motion", "detail", "update"])
+ var appName = '';
+ var moduleName = '';
+ var moduleNames = [];
+ if (patterns.length > 0) {
+ appName = patterns[0];
+ moduleNames = patterns.slice(1);
}
+ if (moduleNames.length > 0) {
+ // convert from camcelcase to dash notation
+ // example: ["motionBlock", "detail"] -> ["motion-block", "detail"]
+ for (var i = 0; i < moduleNames.length; i++) {
+ moduleNames[i] = moduleNames[i].replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase();
+ }
+
+ // use special templateUrl for create and update view
+ // example: ["motion", "detail", "update"] -> "motion-form"
+ if (_.last(moduleNames).match(/(create|update)/)) {
+ moduleName = '/' + moduleNames[0] + '-form';
+ } else {
+ // convert modelNames array to url string
+ // example: ["motion-block", "detail"] -> "motion-block-detail"
+ moduleName = '/' + moduleNames.join('-');
+ }
+ }
+ templateUrl = 'static/templates/' + appName + moduleName + '.html';
config.templateUrl = state.templateUrl || templateUrl;
// controller
if (patterns.length >= 3) {
- controller = _.capitalize(patterns[1]) + defaultControllers[_.last(patterns)];
+ controller = _.upperFirst(patterns[1]) + defaultControllers[_.last(patterns)];
config.controller = state.controller || controller;
}
result[name] = config;
@@ -1417,6 +1436,20 @@ angular.module('OpenSlidesApp.core.site', [
}
])
+.filter('toArray', function(){
+ /*
+ * Transforms an object to an array. Items of the array are the values of
+ * the object elements.
+ */
+ return function(obj) {
+ var result = [];
+ angular.forEach(obj, function(val, key) {
+ result.push(val);
+ });
+ return result;
+ };
+})
+
//Mark all core config strings for translation in Javascript
.config([
'gettext',
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/0006_auto_20161017_2020.py b/openslides/motions/migrations/0006_auto_20161017_2020.py
new file mode 100644
index 000000000..90a0fa6d0
--- /dev/null
+++ b/openslides/motions/migrations/0006_auto_20161017_2020.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.2 on 2016-10-17 18:20
+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', '0005_motionchangerecommendation'),
+ ]
+
+ 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..fdde434ef 100644
--- a/openslides/motions/models.py
+++ b/openslides/motions/models.py
@@ -17,12 +17,14 @@ from openslides.poll.models import (
BaseVote,
CollectDefaultVotesMixin,
)
+from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.models import RESTModelMixin
from openslides.utils.search import user_name_helper
from .access_permissions import (
CategoryAccessPermissions,
MotionAccessPermissions,
+ MotionBlockAccessPermissions,
MotionChangeRecommendationAccessPermissions,
WorkflowAccessPermissions,
)
@@ -116,6 +118,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
@@ -182,7 +193,7 @@ class Motion(RESTModelMixin, models.Model):
return self.title
# TODO: Use transaction
- def save(self, use_version=None, *args, **kwargs):
+ def save(self, use_version=None, skip_autoupdate=False, *args, **kwargs):
"""
Save the motion.
@@ -215,14 +226,19 @@ class Motion(RESTModelMixin, models.Model):
if not self.identifier and isinstance(self.identifier, str):
self.identifier = None
- super(Motion, self).save(*args, **kwargs)
+ # Always skip autoupdate. Maybe we run it later in this method.
+ super(Motion, self).save(skip_autoupdate=True, *args, **kwargs)
if 'update_fields' in kwargs:
# Do not save the version data if only some motion fields are updated.
+ if not skip_autoupdate:
+ inform_changed_data(self)
return
if use_version is False:
# We do not need to save the version.
+ if not skip_autoupdate:
+ inform_changed_data(self)
return
elif use_version is None:
use_version = self.get_last_version()
@@ -239,6 +255,8 @@ class Motion(RESTModelMixin, models.Model):
if use_version.id is None:
if not self.version_data_changed(use_version):
# We do not need to save the version.
+ if not skip_autoupdate:
+ inform_changed_data(self)
return
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
use_version.version_number = version_number + 1
@@ -246,16 +264,22 @@ class Motion(RESTModelMixin, models.Model):
# Necessary line if the version was set before the motion got an id.
use_version.motion = use_version.motion
- use_version.save()
+ # Always skip autoupdate. Maybe we run it later in this method.
+ use_version.save(skip_autoupdate=True)
# Set the active version of this motion. This has to be done after the
# version is saved in the database.
# TODO: Move parts of these last lines of code outside the save method
- # when other versions than the last ones should be edited later on.
+ # when other versions than the last one should be edited later on.
if self.active_version is None or not self.state.leave_old_version_active:
# TODO: Don't call this if it was not a new version
self.active_version = use_version
- self.save(update_fields=['active_version'])
+ # Always skip autoupdate. Maybe we run it later in this method.
+ self.save(update_fields=['active_version'], skip_autoupdate=True)
+
+ # Finally run autoupdate if it is not skipped by caller.
+ if not skip_autoupdate:
+ inform_changed_data(self)
def version_data_changed(self, version):
"""
@@ -520,6 +544,13 @@ class Motion(RESTModelMixin, models.Model):
recommendation = State.objects.get(pk=recommendation)
self.recommendation = recommendation
+ def follow_recommendation(self):
+ """
+ Set the state of this motion to its recommendation.
+ """
+ if self.recommendation is not None:
+ self.set_state(self.recommendation)
+
def get_agenda_title(self):
"""
Return a simple title string for the agenda.
@@ -614,7 +645,7 @@ class Motion(RESTModelMixin, models.Model):
return actions
- def write_log(self, message_list, person=None):
+ def write_log(self, message_list, person=None, skip_autoupdate=False):
"""
Write a log message.
@@ -623,7 +654,8 @@ class Motion(RESTModelMixin, models.Model):
"""
if person and not person.is_authenticated():
person = None
- MotionLog.objects.create(motion=self, message_list=message_list, person=person)
+ motion_log = MotionLog(motion=self, message_list=message_list, person=person)
+ motion_log.save(skip_autoupdate=skip_autoupdate)
def is_amendment(self):
"""
@@ -783,6 +815,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..19e596b23
--- /dev/null
+++ b/openslides/motions/static/js/motions/motion-block.js
@@ -0,0 +1,259 @@
+(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/motion-block-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 ...')
+ }
+ }
+ ];
+ }
+ };
+ }
+])
+
+.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);
+ };
+ }
+])
+
+.controller('MotionBlockDetailCtrl', [
+ '$scope',
+ '$http',
+ 'ngDialog',
+ 'Motion',
+ 'MotionBlockForm',
+ 'MotionBlock',
+ 'motionBlock',
+ 'Projector',
+ 'ProjectionDefault',
+ function($scope, $http, ngDialog, Motion, MotionBlockForm, MotionBlock, motionBlock, Projector, ProjectionDefault) {
+ MotionBlock.bindOne(motionBlock.id, $scope, 'motionBlock');
+ Motion.bindAll({}, $scope, 'motions');
+ $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));
+ };
+ $scope.followRecommendations = function () {
+ $http.post('/rest/motions/motion-block/' + motionBlock.id + '/follow_recommendations/')
+ .success(function(data) {
+ $scope.alert = { type: 'success', msg: data.detail, show: true };
+ })
+ .error(function(data) {
+ $scope.alert = { type: 'danger', msg: data.detail, show: true };
+ });
+ };
+ $scope.delete = function (motion) {
+ motion.motion_block_id = null;
+ motion.title = motion.getTitle(-1);
+ motion.text = motion.getText(-1);
+ motion.reason = motion.getReason(-1);
+ Motion.save(motion);
+ };
+ }
+])
+
+.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..a671d12aa 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,72 @@ angular.module('OpenSlidesApp.motions.site', [
},
controller: 'CategorySortCtrl',
templateUrl: 'static/templates/motions/category-sort.html'
+ })
+ // MotionBlock
+ .state('motions.motionBlock', {
+ url: '/blocks',
+ abstract: true,
+ template: '
+
+ {{ motion.identifier }} {{ motion.getTitle() }}
+
+
+
+ Remove from motion block
+
+ |
+
+ {{ motion.state.name | translate }}
+
+ |
+
+ {{ motion.recommendation.recommendation_label | translate }}
+
+ |
+ + {{ motionBlock.title }} + + + | + {{ motionBlock.motions.length }} + |