commit
f2fa8de7be
@ -32,6 +32,9 @@ Motions:
|
|||||||
- Changed label of former state "commited a bill" to "refered to committee".
|
- Changed label of former state "commited a bill" to "refered to committee".
|
||||||
- Added options to calculate percentages on different bases.
|
- Added options to calculate percentages on different bases.
|
||||||
- Added majority calculation.
|
- 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:
|
Users:
|
||||||
- Added field is_committee and new default group Committees.
|
- Added field is_committee and new default group Committees.
|
||||||
|
@ -295,8 +295,9 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
|
|||||||
'Assignment', // TODO: Remove this after refactoring of data loading on start.
|
'Assignment', // TODO: Remove this after refactoring of data loading on start.
|
||||||
'Topic', // 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.
|
'Motion', // TODO: Remove this after refactoring of data loading on start.
|
||||||
|
'MotionBlock', // TODO: Remove this after refactoring of data loading on start.
|
||||||
'Agenda',
|
'Agenda',
|
||||||
function (Projector, Assignment, Topic, Motion, Agenda) {
|
function (Projector, Assignment, Topic, Motion, MotionBlock, Agenda) {
|
||||||
return {
|
return {
|
||||||
getItem: function (projectorId) {
|
getItem: function (projectorId) {
|
||||||
var elementPromise;
|
var elementPromise;
|
||||||
@ -311,6 +312,13 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
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':
|
case 'topics/topic':
|
||||||
elementPromise = Topic.find(element.id).then(function(topic) {
|
elementPromise = Topic.find(element.id).then(function(topic) {
|
||||||
return Topic.loadRelations(topic, 'agenda_item').then(function() {
|
return Topic.loadRelations(topic, 'agenda_item').then(function() {
|
||||||
|
@ -64,6 +64,10 @@ def create_builtin_projection_defaults(**kwargs):
|
|||||||
name='motions',
|
name='motions',
|
||||||
display_name='Motions',
|
display_name='Motions',
|
||||||
projector=default_projector)
|
projector=default_projector)
|
||||||
|
ProjectionDefault.objects.create(
|
||||||
|
name='motionBlocks',
|
||||||
|
display_name='Motion Blocks',
|
||||||
|
projector=default_projector)
|
||||||
ProjectionDefault.objects.create(
|
ProjectionDefault.objects.create(
|
||||||
name='assignments',
|
name='assignments',
|
||||||
display_name='Elections',
|
display_name='Elections',
|
||||||
|
@ -165,10 +165,8 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
angular.forEach(views, function(config, name) {
|
angular.forEach(views, function(config, name) {
|
||||||
|
// Sets additional default values for templateUrl
|
||||||
// Sets default values for templateUrl
|
var templateUrl,
|
||||||
var patterns = state.name.split('.'),
|
|
||||||
templateUrl,
|
|
||||||
controller,
|
controller,
|
||||||
defaultControllers = {
|
defaultControllers = {
|
||||||
create: 'CreateCtrl',
|
create: 'CreateCtrl',
|
||||||
@ -177,22 +175,43 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
detail: 'DetailCtrl',
|
detail: 'DetailCtrl',
|
||||||
};
|
};
|
||||||
|
|
||||||
// templateUrl
|
// Split up state name
|
||||||
if (_.last(patterns).match(/(create|update)/)) {
|
// example: "motions.motion.detail.update" -> ['motions', 'motion', 'detail', 'update']
|
||||||
// When state_patterns is in the form "app.module.create" or
|
var patterns = state.name.split('.');
|
||||||
// "app.module.update", use the form template.
|
|
||||||
templateUrl = 'static/templates/' + patterns[0] + '/' + patterns[1] + '-form.html';
|
// set app and module name from state
|
||||||
} else {
|
// - appName: patterns[0] (e.g. "motions")
|
||||||
// Replaces the first point through a slash (the app name)
|
// - moduleNames: patterns without first element (e.g. ["motion", "detail", "update"])
|
||||||
var appName = state.name.replace('.', '/');
|
var appName = '';
|
||||||
// Replaces any folowing points though a -
|
var moduleName = '';
|
||||||
templateUrl = 'static/templates/' + appName.replace(/\./g, '-') + '.html';
|
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;
|
config.templateUrl = state.templateUrl || templateUrl;
|
||||||
|
|
||||||
// controller
|
// controller
|
||||||
if (patterns.length >= 3) {
|
if (patterns.length >= 3) {
|
||||||
controller = _.capitalize(patterns[1]) + defaultControllers[_.last(patterns)];
|
controller = _.upperFirst(patterns[1]) + defaultControllers[_.last(patterns)];
|
||||||
config.controller = state.controller || controller;
|
config.controller = state.controller || controller;
|
||||||
}
|
}
|
||||||
result[name] = config;
|
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
|
//Mark all core config strings for translation in Javascript
|
||||||
.config([
|
.config([
|
||||||
'gettext',
|
'gettext',
|
||||||
|
@ -93,6 +93,25 @@ class CategoryAccessPermissions(BaseAccessPermissions):
|
|||||||
return CategorySerializer
|
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):
|
class WorkflowAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for Workflow and WorkflowViewSet.
|
Access permissions container for Workflow and WorkflowViewSet.
|
||||||
|
@ -18,7 +18,7 @@ class MotionsAppConfig(AppConfig):
|
|||||||
from openslides.utils.rest_api import router
|
from openslides.utils.rest_api import router
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
from .signals import create_builtin_workflows
|
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
|
# Define config variables
|
||||||
config.update_config_variables(get_config_variables())
|
config.update_config_variables(get_config_variables())
|
||||||
@ -29,6 +29,7 @@ class MotionsAppConfig(AppConfig):
|
|||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet)
|
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('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('Workflow').get_collection_string(), WorkflowViewSet)
|
||||||
router.register(self.get_model('MotionChangeRecommendation').get_collection_string(),
|
router.register(self.get_model('MotionChangeRecommendation').get_collection_string(),
|
||||||
MotionChangeRecommendationViewSet)
|
MotionChangeRecommendationViewSet)
|
||||||
|
33
openslides/motions/migrations/0006_auto_20161017_2020.py
Normal file
33
openslides/motions/migrations/0006_auto_20161017_2020.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -17,12 +17,14 @@ from openslides.poll.models import (
|
|||||||
BaseVote,
|
BaseVote,
|
||||||
CollectDefaultVotesMixin,
|
CollectDefaultVotesMixin,
|
||||||
)
|
)
|
||||||
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.search import user_name_helper
|
from openslides.utils.search import user_name_helper
|
||||||
|
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
|
MotionBlockAccessPermissions,
|
||||||
MotionChangeRecommendationAccessPermissions,
|
MotionChangeRecommendationAccessPermissions,
|
||||||
WorkflowAccessPermissions,
|
WorkflowAccessPermissions,
|
||||||
)
|
)
|
||||||
@ -116,6 +118,15 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
ForeignKey to one category of motions.
|
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)
|
origin = models.CharField(max_length=255, blank=True)
|
||||||
"""
|
"""
|
||||||
A string to describe the origin of this motion e. g. that it was
|
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
|
return self.title
|
||||||
|
|
||||||
# TODO: Use transaction
|
# 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.
|
Save the motion.
|
||||||
|
|
||||||
@ -215,14 +226,19 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
if not self.identifier and isinstance(self.identifier, str):
|
if not self.identifier and isinstance(self.identifier, str):
|
||||||
self.identifier = None
|
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:
|
if 'update_fields' in kwargs:
|
||||||
# Do not save the version data if only some motion fields are updated.
|
# Do not save the version data if only some motion fields are updated.
|
||||||
|
if not skip_autoupdate:
|
||||||
|
inform_changed_data(self)
|
||||||
return
|
return
|
||||||
|
|
||||||
if use_version is False:
|
if use_version is False:
|
||||||
# We do not need to save the version.
|
# We do not need to save the version.
|
||||||
|
if not skip_autoupdate:
|
||||||
|
inform_changed_data(self)
|
||||||
return
|
return
|
||||||
elif use_version is None:
|
elif use_version is None:
|
||||||
use_version = self.get_last_version()
|
use_version = self.get_last_version()
|
||||||
@ -239,6 +255,8 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
if use_version.id is None:
|
if use_version.id is None:
|
||||||
if not self.version_data_changed(use_version):
|
if not self.version_data_changed(use_version):
|
||||||
# We do not need to save the version.
|
# We do not need to save the version.
|
||||||
|
if not skip_autoupdate:
|
||||||
|
inform_changed_data(self)
|
||||||
return
|
return
|
||||||
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
|
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
|
||||||
use_version.version_number = version_number + 1
|
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.
|
# Necessary line if the version was set before the motion got an id.
|
||||||
use_version.motion = use_version.motion
|
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
|
# Set the active version of this motion. This has to be done after the
|
||||||
# version is saved in the database.
|
# version is saved in the database.
|
||||||
# TODO: Move parts of these last lines of code outside the save method
|
# 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:
|
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
|
# TODO: Don't call this if it was not a new version
|
||||||
self.active_version = use_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):
|
def version_data_changed(self, version):
|
||||||
"""
|
"""
|
||||||
@ -520,6 +544,13 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
recommendation = State.objects.get(pk=recommendation)
|
recommendation = State.objects.get(pk=recommendation)
|
||||||
self.recommendation = 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):
|
def get_agenda_title(self):
|
||||||
"""
|
"""
|
||||||
Return a simple title string for the agenda.
|
Return a simple title string for the agenda.
|
||||||
@ -614,7 +645,7 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
return actions
|
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.
|
Write a log message.
|
||||||
|
|
||||||
@ -623,7 +654,8 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
if person and not person.is_authenticated():
|
if person and not person.is_authenticated():
|
||||||
person = None
|
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):
|
def is_amendment(self):
|
||||||
"""
|
"""
|
||||||
@ -783,6 +815,66 @@ class Category(RESTModelMixin, models.Model):
|
|||||||
return self.name
|
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):
|
class MotionLog(RESTModelMixin, models.Model):
|
||||||
"""Save a logmessage for a motion."""
|
"""Save a logmessage for a motion."""
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from ..core.exceptions import ProjectorException
|
from ..core.exceptions import ProjectorException
|
||||||
from ..utils.collection import CollectionElement
|
from ..utils.collection import CollectionElement
|
||||||
from ..utils.projector import ProjectorElement
|
from ..utils.projector import ProjectorElement
|
||||||
from .models import Motion
|
from .models import Motion, MotionBlock
|
||||||
|
|
||||||
|
|
||||||
class MotionSlide(ProjectorElement):
|
class MotionSlide(ProjectorElement):
|
||||||
@ -48,3 +48,44 @@ class MotionSlide(ProjectorElement):
|
|||||||
else:
|
else:
|
||||||
data = {'agenda_item_id': motion.agenda_item_id}
|
data = {'agenda_item_id': motion.agenda_item_id}
|
||||||
return data
|
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
|
||||||
|
@ -16,6 +16,7 @@ from openslides.utils.rest_api import (
|
|||||||
from .models import (
|
from .models import (
|
||||||
Category,
|
Category,
|
||||||
Motion,
|
Motion,
|
||||||
|
MotionBlock,
|
||||||
MotionChangeRecommendation,
|
MotionChangeRecommendation,
|
||||||
MotionLog,
|
MotionLog,
|
||||||
MotionPoll,
|
MotionPoll,
|
||||||
@ -42,6 +43,15 @@ class CategorySerializer(ModelSerializer):
|
|||||||
fields = ('id', 'name', 'prefix',)
|
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):
|
class StateSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.State objects.
|
Serializer for motion.models.State objects.
|
||||||
@ -275,6 +285,7 @@ class MotionSerializer(ModelSerializer):
|
|||||||
'active_version',
|
'active_version',
|
||||||
'parent',
|
'parent',
|
||||||
'category',
|
'category',
|
||||||
|
'motion_block',
|
||||||
'origin',
|
'origin',
|
||||||
'submitters',
|
'submitters',
|
||||||
'supporters',
|
'supporters',
|
||||||
@ -300,6 +311,7 @@ class MotionSerializer(ModelSerializer):
|
|||||||
motion.reason = validated_data.get('reason', '')
|
motion.reason = validated_data.get('reason', '')
|
||||||
motion.identifier = validated_data.get('identifier')
|
motion.identifier = validated_data.get('identifier')
|
||||||
motion.category = validated_data.get('category')
|
motion.category = validated_data.get('category')
|
||||||
|
motion.motion_block = validated_data.get('motion_block')
|
||||||
motion.origin = validated_data.get('origin', '')
|
motion.origin = validated_data.get('origin', '')
|
||||||
motion.comments = validated_data.get('comments')
|
motion.comments = validated_data.get('comments')
|
||||||
motion.parent = validated_data.get('parent')
|
motion.parent = validated_data.get('parent')
|
||||||
@ -319,8 +331,8 @@ class MotionSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
Customized method to update a motion.
|
Customized method to update a motion.
|
||||||
"""
|
"""
|
||||||
# Identifier, category, origin and comments.
|
# Identifier, category, motion_block, origin and comments.
|
||||||
for key in ('identifier', 'category', 'origin', 'comments'):
|
for key in ('identifier', 'category', 'motion_block', 'origin', 'comments'):
|
||||||
if key in validated_data.keys():
|
if key in validated_data.keys():
|
||||||
setattr(motion, key, validated_data[key])
|
setattr(motion, key, validated_data[key])
|
||||||
|
|
||||||
|
@ -3,10 +3,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
angular.module('OpenSlidesApp.motions', [
|
angular.module('OpenSlidesApp.motions', [
|
||||||
'OpenSlidesApp.users',
|
'OpenSlidesApp.motions.motionBlock',
|
||||||
'OpenSlidesApp.motions.lineNumbering',
|
'OpenSlidesApp.motions.lineNumbering',
|
||||||
'OpenSlidesApp.motions.diff',
|
'OpenSlidesApp.motions.diff',
|
||||||
'OpenSlidesApp.motions.DOCX'
|
'OpenSlidesApp.motions.DOCX',
|
||||||
|
'OpenSlidesApp.users',
|
||||||
])
|
])
|
||||||
|
|
||||||
.factory('WorkflowState', [
|
.factory('WorkflowState', [
|
||||||
@ -389,6 +390,10 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
localField: 'category',
|
localField: 'category',
|
||||||
localKey: 'category_id',
|
localKey: 'category_id',
|
||||||
},
|
},
|
||||||
|
'motions/motion-block': {
|
||||||
|
localField: 'motionBlock',
|
||||||
|
localKey: 'motion_block_id',
|
||||||
|
},
|
||||||
'agenda/item': {
|
'agenda/item': {
|
||||||
localKey: 'agenda_item_id',
|
localKey: 'agenda_item_id',
|
||||||
localField: 'agenda_item',
|
localField: 'agenda_item',
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
}());
|
259
openslides/motions/static/js/motions/motion-block.js
Normal file
259
openslides/motions/static/js/motions/motion-block.js
Normal file
@ -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};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
}());
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
|
angular.module('OpenSlidesApp.motions.projector', [
|
||||||
|
'OpenSlidesApp.motions',
|
||||||
|
'OpenSlidesApp.motions.motionBlockProjector',
|
||||||
|
])
|
||||||
|
|
||||||
.config([
|
.config([
|
||||||
'slidesProvider',
|
'slidesProvider',
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
angular.module('OpenSlidesApp.motions.site', [
|
angular.module('OpenSlidesApp.motions.site', [
|
||||||
'OpenSlidesApp.motions',
|
'OpenSlidesApp.motions',
|
||||||
'OpenSlidesApp.motions.diff',
|
|
||||||
'OpenSlidesApp.motions.motionservices',
|
'OpenSlidesApp.motions.motionservices',
|
||||||
'OpenSlidesApp.poll.majority',
|
'OpenSlidesApp.poll.majority',
|
||||||
'OpenSlidesApp.core.pdf',
|
'OpenSlidesApp.core.pdf',
|
||||||
@ -50,6 +49,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
categories: function(Category) {
|
categories: function(Category) {
|
||||||
return Category.findAll();
|
return Category.findAll();
|
||||||
},
|
},
|
||||||
|
motionBlocks: function(MotionBlock) {
|
||||||
|
return MotionBlock.findAll();
|
||||||
|
},
|
||||||
tags: function(Tag) {
|
tags: function(Tag) {
|
||||||
return Tag.findAll();
|
return Tag.findAll();
|
||||||
},
|
},
|
||||||
@ -81,6 +83,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
categories: function(Category) {
|
categories: function(Category) {
|
||||||
return Category.findAll();
|
return Category.findAll();
|
||||||
},
|
},
|
||||||
|
motionBlocks: function(MotionBlock) {
|
||||||
|
return MotionBlock.findAll();
|
||||||
|
},
|
||||||
users: function(User) {
|
users: function(User) {
|
||||||
return User.findAll().catch(
|
return User.findAll().catch(
|
||||||
function () {
|
function () {
|
||||||
@ -198,6 +203,72 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
},
|
},
|
||||||
controller: 'CategorySortCtrl',
|
controller: 'CategorySortCtrl',
|
||||||
templateUrl: 'static/templates/motions/category-sort.html'
|
templateUrl: 'static/templates/motions/category-sort.html'
|
||||||
|
})
|
||||||
|
// MotionBlock
|
||||||
|
.state('motions.motionBlock', {
|
||||||
|
url: '/blocks',
|
||||||
|
abstract: true,
|
||||||
|
template: '<ui-view/>',
|
||||||
|
})
|
||||||
|
.state('motions.motionBlock.list', {
|
||||||
|
resolve: {
|
||||||
|
motionBlocks: function (MotionBlock) {
|
||||||
|
return MotionBlock.findAll();
|
||||||
|
},
|
||||||
|
motions: function(Motion) {
|
||||||
|
return Motion.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);
|
||||||
|
},
|
||||||
|
motions: function(Motion) {
|
||||||
|
return Motion.findAll();
|
||||||
|
},
|
||||||
|
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/motion-block-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 +377,13 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'Category',
|
'Category',
|
||||||
'Config',
|
'Config',
|
||||||
'Mediafile',
|
'Mediafile',
|
||||||
|
'MotionBlock',
|
||||||
'Tag',
|
'Tag',
|
||||||
'User',
|
'User',
|
||||||
'Workflow',
|
'Workflow',
|
||||||
'Agenda',
|
'Agenda',
|
||||||
'AgendaTree',
|
'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 {
|
return {
|
||||||
// ngDialog for motion form
|
// ngDialog for motion form
|
||||||
getDialog: function (motion) {
|
getDialog: function (motion) {
|
||||||
@ -453,6 +525,17 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
},
|
},
|
||||||
hideExpression: '!model.more'
|
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',
|
key: 'origin',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
@ -630,6 +713,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'Workflow',
|
'Workflow',
|
||||||
'User',
|
'User',
|
||||||
'Agenda',
|
'Agenda',
|
||||||
|
'MotionBlock',
|
||||||
'MotionDocxExport',
|
'MotionDocxExport',
|
||||||
'MotionContentProvider',
|
'MotionContentProvider',
|
||||||
'MotionCatalogContentProvider',
|
'MotionCatalogContentProvider',
|
||||||
@ -639,11 +723,12 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'HTMLValidizer',
|
'HTMLValidizer',
|
||||||
'Projector',
|
'Projector',
|
||||||
'ProjectionDefault',
|
'ProjectionDefault',
|
||||||
function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda, MotionDocxExport,
|
function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda, MotionBlock,
|
||||||
MotionContentProvider, MotionCatalogContentProvider, PdfMakeConverter, PdfMakeDocumentProvider,
|
MotionDocxExport, MotionContentProvider, MotionCatalogContentProvider, PdfMakeConverter, PdfMakeDocumentProvider,
|
||||||
gettextCatalog, HTMLValidizer, Projector, ProjectionDefault) {
|
gettextCatalog, HTMLValidizer, Projector, ProjectionDefault) {
|
||||||
Motion.bindAll({}, $scope, 'motions');
|
Motion.bindAll({}, $scope, 'motions');
|
||||||
Category.bindAll({}, $scope, 'categories');
|
Category.bindAll({}, $scope, 'categories');
|
||||||
|
MotionBlock.bindAll({}, $scope, 'motionBlocks');
|
||||||
Tag.bindAll({}, $scope, 'tags');
|
Tag.bindAll({}, $scope, 'tags');
|
||||||
Workflow.bindAll({}, $scope, 'workflows');
|
Workflow.bindAll({}, $scope, 'workflows');
|
||||||
User.bindAll({}, $scope, 'users');
|
User.bindAll({}, $scope, 'users');
|
||||||
@ -666,11 +751,13 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
$scope.multiselectFilter = {
|
$scope.multiselectFilter = {
|
||||||
state: [],
|
state: [],
|
||||||
category: [],
|
category: [],
|
||||||
|
motionBlock: [],
|
||||||
tag: []
|
tag: []
|
||||||
};
|
};
|
||||||
$scope.getItemId = {
|
$scope.getItemId = {
|
||||||
state: function (motion) {return motion.state_id;},
|
state: function (motion) {return motion.state_id;},
|
||||||
category: function (motion) {return motion.category_id;},
|
category: function (motion) {return motion.category_id;},
|
||||||
|
motionBlock: function (motion) {return motion.motion_block_id;},
|
||||||
tag: function (motion) {return motion.tags_id;}
|
tag: function (motion) {return motion.tags_id;}
|
||||||
};
|
};
|
||||||
// function to operate the multiselectFilter
|
// function to operate the multiselectFilter
|
||||||
@ -700,6 +787,10 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
if (motion.category) {
|
if (motion.category) {
|
||||||
category = motion.category.name;
|
category = motion.category.name;
|
||||||
}
|
}
|
||||||
|
var motionBlock = '';
|
||||||
|
if (motion.motionBlock) {
|
||||||
|
motionBlock = motion.motionBlock.title;
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
motion.identifier,
|
motion.identifier,
|
||||||
motion.getTitle(),
|
motion.getTitle(),
|
||||||
@ -725,6 +816,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
}
|
}
|
||||||
).join(" "),
|
).join(" "),
|
||||||
category,
|
category,
|
||||||
|
motionBlock
|
||||||
].join(" ");
|
].join(" ");
|
||||||
};
|
};
|
||||||
// for reset-button
|
// for reset-button
|
||||||
@ -732,6 +824,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
$scope.multiselectFilter = {
|
$scope.multiselectFilter = {
|
||||||
state: [],
|
state: [],
|
||||||
category: [],
|
category: [],
|
||||||
|
motionBlock: [],
|
||||||
tag: []
|
tag: []
|
||||||
};
|
};
|
||||||
if ($scope.filter) {
|
if ($scope.filter) {
|
||||||
@ -741,6 +834,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
$scope.are_filters_set = function () {
|
$scope.are_filters_set = function () {
|
||||||
return $scope.multiselectFilter.state.length > 0 ||
|
return $scope.multiselectFilter.state.length > 0 ||
|
||||||
$scope.multiselectFilter.category.length > 0 ||
|
$scope.multiselectFilter.category.length > 0 ||
|
||||||
|
$scope.multiselectFilter.motionBlock.length > 0 ||
|
||||||
$scope.multiselectFilter.tag.length > 0 ||
|
$scope.multiselectFilter.tag.length > 0 ||
|
||||||
($scope.filter ? $scope.filter.search : false);
|
($scope.filter ? $scope.filter.search : false);
|
||||||
};
|
};
|
||||||
@ -801,6 +895,14 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
}
|
}
|
||||||
save(motion);
|
save(motion);
|
||||||
};
|
};
|
||||||
|
$scope.toggle_motionBlock = function (motion, block) {
|
||||||
|
if (motion.motion_block_id == block.id) {
|
||||||
|
motion.motion_block_id = null;
|
||||||
|
} else {
|
||||||
|
motion.motion_block_id = block.id;
|
||||||
|
}
|
||||||
|
save(motion);
|
||||||
|
};
|
||||||
|
|
||||||
// open new/edit dialog
|
// open new/edit dialog
|
||||||
$scope.openDialog = function (motion) {
|
$scope.openDialog = function (motion) {
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
<div class="submenu">
|
||||||
|
<a ui-sref="motions.motionBlock.list" class="btn btn-sm btn-default">
|
||||||
|
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||||
|
<translate>Back to overview</translate>
|
||||||
|
</a>
|
||||||
|
<!-- List of speakers -->
|
||||||
|
<a ui-sref="agenda.item.detail({id: motionBlock.agenda_item_id})" class="btn btn-sm btn-default">
|
||||||
|
<i class="fa fa-microphone fa-lg"></i>
|
||||||
|
<translate>List of speakers</translate>
|
||||||
|
</a>
|
||||||
|
<!-- project -->
|
||||||
|
<projector-button model="motionBlock" default-projector-id="defaultProjectorId">
|
||||||
|
</projector-button>
|
||||||
|
<!-- edit -->
|
||||||
|
<a ng-click="openDialog(motionBlock)"
|
||||||
|
class="btn btn-default btn-sm"
|
||||||
|
title="{{ 'Edit' | translate}}">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1>{{ motionBlock.agenda_item.getTitle() }}</h1>
|
||||||
|
<h2 translate>Motion block</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details">
|
||||||
|
<a os-perms="motions.can_manage" class="btn btn-default btn"
|
||||||
|
ng-bootbox-confirm="{{ 'Are you sure you want to override the state of all motions of this motion block?' | translate }}"
|
||||||
|
ng-bootbox-confirm-action="followRecommendations()" translate>
|
||||||
|
<i class="fa fa-magic fa-lg"></i>
|
||||||
|
<translate>Follow recommendations for all motions</translate>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="row spacer form-group">
|
||||||
|
<div class="col-sm-4 pull-right">
|
||||||
|
<input type="text" ng-model="filter.search" class="form-control"
|
||||||
|
placeholder="{{ 'Filter' | translate }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 italic">
|
||||||
|
{{ motionsFiltered.length }} /
|
||||||
|
{{ motionBlock.motions.length }} {{ "motions" | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-striped table-bordered table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><translate>Motion</translate>
|
||||||
|
<th><translate>State</translate>
|
||||||
|
<th><translate>Recommendation</translate>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="motion in motionsFiltered = (motionBlock.motions | filter: filter.search | orderBy: 'identifier')">
|
||||||
|
<td ng-mouseover="motion.hover=true" ng-mouseleave="motion.hover=false">
|
||||||
|
<strong>
|
||||||
|
<a ui-sref="motions.motion.detail({id: motion.id})">{{ motion.identifier }} {{ motion.getTitle() }}</a>
|
||||||
|
</strong>
|
||||||
|
<div os-perms="motions.can_manage" class="hoverActions" ng-class="{'hiddenDiv': !motion.hover}">
|
||||||
|
<!-- delete -->
|
||||||
|
<a href="" class="text-danger"
|
||||||
|
ng-bootbox-confirm="{{ 'Are you sure you want to remove this motion from motion block?' | translate }}<br>
|
||||||
|
<b>{{ motion.getTitle() }}</b>"
|
||||||
|
ng-bootbox-confirm-action="delete(motion)" translate>Remove from motion block</a>
|
||||||
|
</div>
|
||||||
|
<td>
|
||||||
|
<div class="label" ng-class="'label-'+motion.state.css_class">
|
||||||
|
{{ motion.state.name | translate }}
|
||||||
|
</div>
|
||||||
|
<td>
|
||||||
|
<div class="label" ng-class="'label-'+motion.recommendation.css_class">
|
||||||
|
{{ motion.recommendation.recommendation_label | translate }}
|
||||||
|
</div>
|
||||||
|
</table>
|
||||||
|
</div>
|
@ -0,0 +1,17 @@
|
|||||||
|
<h1 ng-if="model.id" translate>Edit motion block</h1>
|
||||||
|
<h1 ng-if="!model.id" translate>New motion block</h1>
|
||||||
|
|
||||||
|
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
||||||
|
{{ alert.msg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form name="motionBlockForm" ng-submit="save(model)">
|
||||||
|
<formly-form model="model" fields="formFields">
|
||||||
|
<button type="submit" ng-disabled="motionBlockForm.$invalid" class="btn btn-primary" translate>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button ng-click="closeThisDialog()" class="btn btn-default" translate>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</formly-form>
|
||||||
|
</form>
|
@ -0,0 +1,48 @@
|
|||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
<div class="submenu">
|
||||||
|
<a ng-click="openFormDialog()" os-perms="motions.can_manage" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fa fa-plus fa-lg"></i>
|
||||||
|
<translate>New</translate>
|
||||||
|
</a>
|
||||||
|
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default">
|
||||||
|
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||||
|
<translate>Back to overview</translate>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1 translate>Motion blocks</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details">
|
||||||
|
<div class="row form-group">
|
||||||
|
<div class="col-sm-4 pull-right">
|
||||||
|
<input type="text" ng-model="filter.search" class="form-control"
|
||||||
|
placeholder="{{ 'Filter' | translate }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-striped table-bordered table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><translate>Name</translate>
|
||||||
|
<th><translate>Motions</translate>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="motionBlock in motionBlocks | filter: filter.search | orderBy: 'title'">
|
||||||
|
<td ng-mouseover="motionBlock.hover=true" ng-mouseleave="motionBlock.hover=false">
|
||||||
|
<strong>
|
||||||
|
<a ui-sref="motions.motionBlock.detail({id: motionBlock.id})">{{ motionBlock.title }}</a>
|
||||||
|
</strong>
|
||||||
|
<div os-perms="motions.can_manage" class="hoverActions" ng-class="{'hiddenDiv': !motionBlock.hover}">
|
||||||
|
<!-- edit -->
|
||||||
|
<a ng-click="openFormDialog(motionBlock)" translate>Edit</a> |
|
||||||
|
<!-- delete -->
|
||||||
|
<a href="" class="text-danger"
|
||||||
|
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||||
|
<b>{{ motionBlock.name }}</b>"
|
||||||
|
ng-bootbox-confirm-action="delete(motionBlock)" translate>Delete</a>
|
||||||
|
</div>
|
||||||
|
<td>
|
||||||
|
{{ motionBlock.motions.length }}
|
||||||
|
</table>
|
||||||
|
</div>
|
@ -150,6 +150,12 @@
|
|||||||
<h3 ng-if="motion.category" translate>Category</h3>
|
<h3 ng-if="motion.category" translate>Category</h3>
|
||||||
{{ motion.category.name }}
|
{{ motion.category.name }}
|
||||||
|
|
||||||
|
<!-- Motion block -->
|
||||||
|
<h3 translate>Motion block</h3>
|
||||||
|
<a ui-sref="motions.motionBlock.detail({id: motion.motionBlock.id})"
|
||||||
|
os-perms="motions.can_manage">{{ motion.motionBlock.title }}</a>
|
||||||
|
<span os-perms="!motions.can_manage">{{ motion.motionBlock.title }}</span>
|
||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<h3 ng-if="motion.tags.length > 0" translate>Tags</h3>
|
<h3 ng-if="motion.tags.length > 0" translate>Tags</h3>
|
||||||
<span ng-repeat="tag in motion.tags">
|
<span ng-repeat="tag in motion.tags">
|
||||||
@ -159,6 +165,7 @@
|
|||||||
<!-- Origin -->
|
<!-- Origin -->
|
||||||
<h3 ng-if="motion.origin" translate>Origin</h3>
|
<h3 ng-if="motion.origin" translate>Origin</h3>
|
||||||
{{ motion.origin }}
|
{{ motion.origin }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h3 ng-if="motion.polls.length > 0" translate>Voting result</h3>
|
<h3 ng-if="motion.polls.length > 0" translate>Voting result</h3>
|
||||||
|
@ -9,6 +9,10 @@
|
|||||||
<i class="fa fa-sitemap fa-lg"></i>
|
<i class="fa fa-sitemap fa-lg"></i>
|
||||||
<translate>Categories</translate>
|
<translate>Categories</translate>
|
||||||
</a>
|
</a>
|
||||||
|
<a ui-sref="motions.motionBlock.list" os-perms="motions.can_manage" class="btn btn-default btn-sm">
|
||||||
|
<i class="fa fa-th-large fa-lg"></i>
|
||||||
|
<translate>Motion blocks</translate>
|
||||||
|
</a>
|
||||||
<a ui-sref="core.tag.list" os-perms="core.can_manage_tags" class="btn btn-default btn-sm">
|
<a ui-sref="core.tag.list" os-perms="core.can_manage_tags" class="btn btn-default btn-sm">
|
||||||
<i class="fa fa-tags fa-lg"></i>
|
<i class="fa fa-tags fa-lg"></i>
|
||||||
<translate>Tags</translate>
|
<translate>Tags</translate>
|
||||||
@ -145,6 +149,25 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
|
<!-- Motion block filter -->
|
||||||
|
<span class="dropdown" uib-dropdown ng-if="motionBlocks.length > 0">
|
||||||
|
<span class="pointer" id="dropdownBlock" uib-dropdown-toggle
|
||||||
|
ng-class="{'bold': multiselectFilter.motionBlock.length > 0, 'disabled': isDeleteMode}"
|
||||||
|
ng-disabled="isDeleteMode">
|
||||||
|
<translate>Motion block</translate>
|
||||||
|
<span class="caret"></span>
|
||||||
|
</span>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-right"
|
||||||
|
aria-labelledby="dropdownBlock">
|
||||||
|
<li ng-repeat="block in motionBlocks">
|
||||||
|
<div class="dropdown-entry pointer"
|
||||||
|
ng-click="operateMultiselectFilter('motionBlock', block.id)">
|
||||||
|
<i class="fa fa-check" ng-if="multiselectFilter.motionBlock.indexOf(block.id) > -1"></i>
|
||||||
|
{{ block.title }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
<!-- Tag filter -->
|
<!-- Tag filter -->
|
||||||
<span class="dropdown" uib-dropdown ng-if="tags.length > 0">
|
<span class="dropdown" uib-dropdown ng-if="tags.length > 0">
|
||||||
<span class="pointer" id="dropdownTag" uib-dropdown-toggle
|
<span class="pointer" id="dropdownTag" uib-dropdown-toggle
|
||||||
@ -219,11 +242,22 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<!-- category -->
|
<!-- category -->
|
||||||
<div class="pointer dropdown-entry" ng-click="toggleSort('category')">
|
<div class="pointer dropdown-entry" ng-click="toggleSort('category.name')">
|
||||||
<translate>Category</translate>
|
<translate>Category</translate>
|
||||||
<span class="spacer-right pull-right"></span>
|
<span class="spacer-right pull-right"></span>
|
||||||
<i class="pull-right fa"
|
<i class="pull-right fa"
|
||||||
ng-style="{'visibility': sortColumn === 'category' && header.sortable != false ? 'visible' : 'hidden'}"
|
ng-style="{'visibility': sortColumn === 'category.name' && header.sortable != false ? 'visible' : 'hidden'}"
|
||||||
|
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<!-- motion block -->
|
||||||
|
<div class="pointer dropdown-entry" ng-click="toggleSort('motionBlock.title')">
|
||||||
|
<translate>Motion block</translate>
|
||||||
|
<span class="spacer-right pull-right"></span>
|
||||||
|
<i class="pull-right fa"
|
||||||
|
ng-style="{'visibility': sortColumn === 'motionBlock.title' && header.sortable != false ? 'visible' : 'hidden'}"
|
||||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
@ -270,6 +304,15 @@
|
|||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span ng-repeat="motionBlock in motionBlocks" class="pointer spacer-left-lg"
|
||||||
|
ng-if="multiselectFilter.motionBlock.indexOf(motionBlock.id) > -1"
|
||||||
|
ng-click="operateMultiselectFilter('motionBlock', motionBlock.id)"
|
||||||
|
ng-class="{'disabled': isDeleteMode}">
|
||||||
|
<span class="nobr">
|
||||||
|
<i class="fa fa-times-circle"></i>
|
||||||
|
{{ motionBlock.title }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<span ng-repeat="tag in tags" class="pointer spacer-left-lg"
|
<span ng-repeat="tag in tags" class="pointer spacer-left-lg"
|
||||||
ng-if="multiselectFilter.tag.indexOf(tag.id) > -1"
|
ng-if="multiselectFilter.tag.indexOf(tag.id) > -1"
|
||||||
ng-click="operateMultiselectFilter('tag', tag.id)"
|
ng-click="operateMultiselectFilter('tag', tag.id)"
|
||||||
@ -292,7 +335,9 @@
|
|||||||
| osFilter: filter.search : getFilterString
|
| osFilter: filter.search : getFilterString
|
||||||
| SelectMultipleFilter: multiselectFilter.state : getItemId.state
|
| SelectMultipleFilter: multiselectFilter.state : getItemId.state
|
||||||
| SelectMultipleFilter: multiselectFilter.category : getItemId.category
|
| SelectMultipleFilter: multiselectFilter.category : getItemId.category
|
||||||
|
| SelectMultipleFilter: multiselectFilter.motionBlock : getItemId.motionBlock
|
||||||
| SelectMultipleFilter: multiselectFilter.tag : getItemId.tag
|
| SelectMultipleFilter: multiselectFilter.tag : getItemId.tag
|
||||||
|
| toArray
|
||||||
| orderBy: sortColumn : reverse)">
|
| orderBy: sortColumn : reverse)">
|
||||||
|
|
||||||
<!-- select column -->
|
<!-- select column -->
|
||||||
@ -310,7 +355,7 @@
|
|||||||
<div class="col-xs-6 content">
|
<div class="col-xs-6 content">
|
||||||
<div class="identifier-col">
|
<div class="identifier-col">
|
||||||
<div class="nobr" ng-show="motion.identifier">
|
<div class="nobr" ng-show="motion.identifier">
|
||||||
{{ motion.identifier }}:
|
{{ motion.identifier }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
@ -411,6 +456,41 @@
|
|||||||
{{ motion.category.name }}
|
{{ motion.category.name }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Motion block dropdown for manage user -->
|
||||||
|
<div os-perms="motions.can_manage" ng-show="motionBlocks.length > 0"
|
||||||
|
ng-mouseover="motion.motionBlockHover=true"
|
||||||
|
ng-mouseleave="motion.motionBlockHover=false">
|
||||||
|
<span uib-dropdown >
|
||||||
|
<span id="dropdown-motionBlock{{ motion.id }}" class="pointer"
|
||||||
|
uib-dropdown-toggle uib-tooltip="{{ 'Set a motion block' | translate }}"
|
||||||
|
tooltip-class="nobr">
|
||||||
|
<span ng-if="motion.motionBlock == null" ng-show="motion.hover">
|
||||||
|
<i class="fa fa-th-large"></i>
|
||||||
|
<i class="fa fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span ng-if="motion.motionBlock != null">
|
||||||
|
<i class="fa fa-th-large spacer-right"></i>
|
||||||
|
{{ motion.motionBlock.title }}
|
||||||
|
<i class="fa fa-cog fa-lg spacer-left" ng-show="motion.motionBlockHover"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="dropdown-motionBlock{{ motion.id }}">
|
||||||
|
<li ng-repeat="motionBlock in motionBlocks">
|
||||||
|
<div class="dropdown-entry pointer"
|
||||||
|
ng-click="toggle_motionBlock(motion, motionBlock)">
|
||||||
|
<i class="fa fa-check" ng-if="motionBlock.id == motion.motionBlock.id"></i>
|
||||||
|
{{ motionBlock.title }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Motion block string for normal user -->
|
||||||
|
<div os-perms="!motions.can_manage" ng-show="motion.motionBlock != null">
|
||||||
|
<i class="fa fa-sitemap spacer-right"></i>
|
||||||
|
{{ motion.motionBlock.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tag dropdown for manage user -->
|
<!-- Tag dropdown for manage user -->
|
||||||
<div os-perms="motions.can_manage" ng-show="tags.length > 0"
|
<div os-perms="motions.can_manage" ng-show="tags.length > 0"
|
||||||
ng-mouseover="motion.tagHover=true"
|
ng-mouseover="motion.tagHover=true"
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
<div ng-controller="SlideMotionBlockCtrl" class="content scrollcontent">
|
||||||
|
<h1>{{ motionBlock.agenda_item.getTitle() }}</h1>
|
||||||
|
<div ng-repeat="motion in motionBlock.motions">{{ motion.identifier }}</div>
|
||||||
|
</div>
|
@ -9,8 +9,9 @@ from django.utils.translation import ugettext_noop
|
|||||||
from reportlab.platypus import SimpleDocTemplate
|
from reportlab.platypus import SimpleDocTemplate
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from openslides.core.config import config
|
from ..core.config import config
|
||||||
from openslides.utils.rest_api import (
|
from ..utils.autoupdate import inform_changed_data
|
||||||
|
from ..utils.rest_api import (
|
||||||
DestroyModelMixin,
|
DestroyModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
@ -19,11 +20,11 @@ from openslides.utils.rest_api import (
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
)
|
)
|
||||||
from openslides.utils.views import APIView, PDFView, SingleObjectMixin
|
from ..utils.views import APIView, PDFView, SingleObjectMixin
|
||||||
|
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
|
MotionBlockAccessPermissions,
|
||||||
MotionChangeRecommendationAccessPermissions,
|
MotionChangeRecommendationAccessPermissions,
|
||||||
WorkflowAccessPermissions,
|
WorkflowAccessPermissions,
|
||||||
)
|
)
|
||||||
@ -31,6 +32,7 @@ from .exceptions import WorkflowError
|
|||||||
from .models import (
|
from .models import (
|
||||||
Category,
|
Category,
|
||||||
Motion,
|
Motion,
|
||||||
|
MotionBlock,
|
||||||
MotionChangeRecommendation,
|
MotionChangeRecommendation,
|
||||||
MotionPoll,
|
MotionPoll,
|
||||||
MotionVersion,
|
MotionVersion,
|
||||||
@ -89,6 +91,8 @@ class MotionViewSet(ModelViewSet):
|
|||||||
# Non-staff users are not allowed to send submitter or supporter data.
|
# Non-staff users are not allowed to send submitter or supporter data.
|
||||||
self.permission_denied(request)
|
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.
|
# Check permission to send comment data.
|
||||||
if not request.user.has_perm('motions.can_see_and_manage_comments'):
|
if not request.user.has_perm('motions.can_see_and_manage_comments'):
|
||||||
try:
|
try:
|
||||||
@ -373,7 +377,7 @@ class CategoryViewSet(ModelViewSet):
|
|||||||
API endpoint for categories.
|
API endpoint for categories.
|
||||||
|
|
||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update and destroy.
|
partial_update, update, destroy and numbering.
|
||||||
"""
|
"""
|
||||||
access_permissions = CategoryAccessPermissions()
|
access_permissions = CategoryAccessPermissions()
|
||||||
queryset = Category.objects.all()
|
queryset = Category.objects.all()
|
||||||
@ -445,6 +449,54 @@ class CategoryViewSet(ModelViewSet):
|
|||||||
return response
|
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', 'follow_recommendations'):
|
||||||
|
result = (self.request.user.has_perm('motions.can_see') and
|
||||||
|
self.request.user.has_perm('motions.can_manage'))
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
def follow_recommendations(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
View to set the states of all motions of this motion block each to
|
||||||
|
its recommendation. It is a POST request without any data.
|
||||||
|
"""
|
||||||
|
motion_block = self.get_object()
|
||||||
|
instances = []
|
||||||
|
with transaction.atomic():
|
||||||
|
for motion in motion_block.motion_set.all():
|
||||||
|
# Follow recommendation.
|
||||||
|
motion.follow_recommendation()
|
||||||
|
motion.save(skip_autoupdate=True)
|
||||||
|
# Write the log message.
|
||||||
|
motion.write_log(
|
||||||
|
message_list=[ugettext_noop('State set to'), ' ', motion.state.name],
|
||||||
|
person=request.user,
|
||||||
|
skip_autoupdate=True)
|
||||||
|
instances.append(motion)
|
||||||
|
inform_changed_data(instances)
|
||||||
|
return Response({'detail': _('Followed recommendations successfully.')})
|
||||||
|
|
||||||
|
|
||||||
class WorkflowViewSet(ModelViewSet):
|
class WorkflowViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for workflows.
|
API endpoint for workflows.
|
||||||
|
@ -145,7 +145,7 @@ def inform_changed_data(instances, information=None):
|
|||||||
"""
|
"""
|
||||||
root_instances = set()
|
root_instances = set()
|
||||||
if not isinstance(instances, Iterable):
|
if not isinstance(instances, Iterable):
|
||||||
# Make surce instance is an iterable
|
# Make sure instances is an iterable
|
||||||
instances = (instances, )
|
instances = (instances, )
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
try:
|
try:
|
||||||
|
@ -7,7 +7,7 @@ from rest_framework.test import APIClient
|
|||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
from openslides.motions.models import Category, Motion, State
|
from openslides.motions.models import Category, Motion, MotionBlock, State
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
@ -726,3 +726,40 @@ class NumberMotionsInCategory(TestCase):
|
|||||||
self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, None)
|
self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, None)
|
||||||
self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2')
|
self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2')
|
||||||
self.assertEqual(Motion.objects.get(pk=self.motion_3.pk).identifier, 'test_prefix_ahz6tho2mooH8 1')
|
self.assertEqual(Motion.objects.get(pk=self.motion_3.pk).identifier, 'test_prefix_ahz6tho2mooH8 1')
|
||||||
|
|
||||||
|
|
||||||
|
class FollowRecommendationsForMotionBlock(TestCase):
|
||||||
|
"""
|
||||||
|
Tests following the recommendations of motions in an motion block.
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
self.state_id_accepted = 2 # This should be the id of the state 'accepted'.
|
||||||
|
self.state_id_rejected = 3 # This should be the id of the state 'rejected'.
|
||||||
|
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username='admin', password='admin')
|
||||||
|
|
||||||
|
self.motion_block = MotionBlock.objects.create(
|
||||||
|
title='test_motion_block_name_Ufoopiub7quaezaepeic')
|
||||||
|
|
||||||
|
self.motion = Motion(
|
||||||
|
title='test_title_yo8ohy5eifeiyied2AeD',
|
||||||
|
text='test_text_chi1aeth5faPhueQu8oh',
|
||||||
|
motion_block=self.motion_block)
|
||||||
|
self.motion.save()
|
||||||
|
self.motion.set_recommendation(self.state_id_accepted)
|
||||||
|
self.motion.save()
|
||||||
|
|
||||||
|
self.motion_2 = Motion(
|
||||||
|
title='test_title_eith0EemaW8ahZa9Piej',
|
||||||
|
text='test_text_haeho1ohk3ou7pau2Jee',
|
||||||
|
motion_block=self.motion_block)
|
||||||
|
self.motion_2.save()
|
||||||
|
self.motion_2.set_recommendation(self.state_id_rejected)
|
||||||
|
self.motion_2.save()
|
||||||
|
|
||||||
|
def test_follow_recommendations_for_motion_block(self):
|
||||||
|
response = self.client.post(reverse('motionblock-follow-recommendations', args=[self.motion_block.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.id, self.state_id_accepted)
|
||||||
|
self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).state.id, self.state_id_rejected)
|
||||||
|
Loading…
Reference in New Issue
Block a user