New feature blocks for motions.
- Added ListView, DetailView, UpdateForm and connection to agenda item for MotionBlock. - Added slide and projection default. - Added custom manager for motion blocks. - Enabled current list of speakers slide and overlay for motion block.
This commit is contained in:
parent
34b074faec
commit
700c86a24c
@ -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() {
|
||||
|
@ -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',
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
34
openslides/motions/migrations/0005_auto_20161004_2350.py
Normal file
34
openslides/motions/migrations/0005_auto_20161004_2350.py
Normal file
@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.1 on 2016-10-04 21:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import openslides.utils.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0004_auto_20160907_2343'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MotionBlock',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='motion_block',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.MotionBlock'),
|
||||
),
|
||||
]
|
@ -23,6 +23,7 @@ from openslides.utils.search import user_name_helper
|
||||
from .access_permissions import (
|
||||
CategoryAccessPermissions,
|
||||
MotionAccessPermissions,
|
||||
MotionBlockAccessPermissions,
|
||||
MotionChangeRecommendationAccessPermissions,
|
||||
WorkflowAccessPermissions,
|
||||
)
|
||||
@ -116,6 +117,15 @@ class Motion(RESTModelMixin, models.Model):
|
||||
ForeignKey to one category of motions.
|
||||
"""
|
||||
|
||||
motion_block = models.ForeignKey(
|
||||
'MotionBlock',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True)
|
||||
"""
|
||||
ForeignKey to one block of motions.
|
||||
"""
|
||||
|
||||
origin = models.CharField(max_length=255, blank=True)
|
||||
"""
|
||||
A string to describe the origin of this motion e. g. that it was
|
||||
@ -783,6 +793,66 @@ class Category(RESTModelMixin, models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class MotionBlockManager(models.Manager):
|
||||
"""
|
||||
Customized model manager to support our get_full_queryset method.
|
||||
"""
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all motion blocks. In the
|
||||
background the related agenda item is prefetched from the database.
|
||||
"""
|
||||
return self.get_queryset().prefetch_related('agenda_items')
|
||||
|
||||
|
||||
class MotionBlock(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for blocks of motions.
|
||||
"""
|
||||
access_permissions = MotionBlockAccessPermissions()
|
||||
|
||||
objects = MotionBlockManager()
|
||||
|
||||
title = models.CharField(max_length=255)
|
||||
|
||||
# In theory there could be one then more agenda_item. But we support only
|
||||
# one. See the property agenda_item.
|
||||
agenda_items = GenericRelation(Item, related_name='topics')
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def agenda_item(self):
|
||||
"""
|
||||
Returns the related agenda item.
|
||||
"""
|
||||
# We support only one agenda item so just return the first element of
|
||||
# the queryset.
|
||||
return self.agenda_items.all()[0]
|
||||
|
||||
@property
|
||||
def agenda_item_id(self):
|
||||
"""
|
||||
Returns the id of the agenda item object related to this object.
|
||||
"""
|
||||
return self.agenda_item.pk
|
||||
|
||||
def get_agenda_title(self):
|
||||
return self.title
|
||||
|
||||
def get_agenda_list_view_title(self):
|
||||
return self.title
|
||||
|
||||
@classmethod
|
||||
def get_collection_string(cls):
|
||||
# TODO: Fix generic method to support camelCase, #2480.
|
||||
return 'motions/motion-block'
|
||||
|
||||
|
||||
class MotionLog(RESTModelMixin, models.Model):
|
||||
"""Save a logmessage for a motion."""
|
||||
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
244
openslides/motions/static/js/motions/motion-block.js
Normal file
244
openslides/motions/static/js/motions/motion-block.js
Normal file
@ -0,0 +1,244 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.motions.motionBlock', [])
|
||||
|
||||
|
||||
// MotionBlock model
|
||||
|
||||
.factory('MotionBlock', [
|
||||
'DS',
|
||||
'jsDataModel',
|
||||
'gettext',
|
||||
function(DS, jsDataModel, gettext) {
|
||||
var name = 'motions/motion-block';
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
useClass: jsDataModel,
|
||||
verboseName: gettext('Motion block'),
|
||||
methods: {
|
||||
getResourceName: function () {
|
||||
return name;
|
||||
},
|
||||
getAgendaTitle: function () {
|
||||
return this.title;
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
belongsTo: {
|
||||
'agenda/item': {
|
||||
localKey: 'agenda_item_id',
|
||||
localField: 'agenda_item',
|
||||
}
|
||||
},
|
||||
hasMany: {
|
||||
'motions/motion': {
|
||||
localField: 'motions',
|
||||
foreignKey: 'motion_block_id',
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.run(['MotionBlock', function(MotionBlock) {}])
|
||||
|
||||
|
||||
// MotionBlock views (list view, create dialog, update dialog)
|
||||
|
||||
.factory('MotionBlockForm', [
|
||||
'$http',
|
||||
'gettextCatalog',
|
||||
'Agenda',
|
||||
'AgendaTree',
|
||||
function ($http, gettextCatalog, Agenda, AgendaTree) {
|
||||
return {
|
||||
// Get ngDialog configuration.
|
||||
getDialog: function (motionBlock) {
|
||||
return {
|
||||
template: 'static/templates/motions/motionBlock-form.html',
|
||||
controller: (motionBlock) ? 'MotionBlockUpdateCtrl' : 'MotionBlockCreateCtrl',
|
||||
className: 'ngdialog-theme-default wide-form',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
motionBlock: function () {
|
||||
return motionBlock;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
// Get angular-formly fields.
|
||||
getFormFields: function () {
|
||||
return [
|
||||
{
|
||||
key: 'title',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Title')
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'showAsAgendaItem',
|
||||
type: 'checkbox',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Show as agenda item'),
|
||||
description: gettextCatalog.getString('If deactivated it appears as internal item on agenda.')
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'agenda_parent_item_id',
|
||||
type: 'select-single',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Parent item'),
|
||||
options: AgendaTree.getFlatTree(Agenda.getAll()),
|
||||
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
|
||||
placeholder: gettextCatalog.getString('Select a parent item ...')
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// TODO: Rename this to MotionBlockListCtrl after $stateProvider is fixed, see #2479.
|
||||
.controller('MotionblockListCtrl', [
|
||||
'$scope',
|
||||
'ngDialog',
|
||||
'MotionBlock',
|
||||
'MotionBlockForm',
|
||||
function ($scope, ngDialog, MotionBlock, MotionBlockForm) {
|
||||
// Two-way data binding for all MotionBlock instances.
|
||||
MotionBlock.bindAll({}, $scope, 'motionBlocks');
|
||||
|
||||
// Dialog with a form to create or update a MotionBlock instance.
|
||||
$scope.openFormDialog = function (motionBlock) {
|
||||
ngDialog.open(MotionBlockForm.getDialog(motionBlock));
|
||||
};
|
||||
|
||||
// Confirm dialog to delete a MotionBlock instance.
|
||||
$scope.delete = function (motionBlock) {
|
||||
MotionBlock.destroy(motionBlock.id);
|
||||
};
|
||||
|
||||
// TODO: In template we have filter toggleSort reverse sortColumn header. Use this stuff or remove it.
|
||||
}
|
||||
])
|
||||
|
||||
// TODO: Rename this to MotionBlockDetailCtrl after $stateProvider is fixed, see #2479.
|
||||
.controller('MotionblockDetailCtrl', [
|
||||
'$scope',
|
||||
'ngDialog',
|
||||
'MotionBlockForm',
|
||||
'MotionBlock',
|
||||
'motionBlock',
|
||||
'Projector',
|
||||
'ProjectionDefault',
|
||||
function($scope, ngDialog, MotionBlockForm, MotionBlock, motionBlock, Projector, ProjectionDefault) {
|
||||
MotionBlock.bindOne(motionBlock.id, $scope, 'motionBlock');
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified();
|
||||
}, function () {
|
||||
var projectiondefault = ProjectionDefault.filter({name: 'motionBlocks'})[0];
|
||||
if (projectiondefault) {
|
||||
$scope.defaultProjectorId = projectiondefault.projector_id;
|
||||
}
|
||||
});
|
||||
$scope.openDialog = function (topic) {
|
||||
ngDialog.open(MotionBlockForm.getDialog(motionBlock));
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('MotionBlockCreateCtrl', [
|
||||
'$scope',
|
||||
'MotionBlock',
|
||||
'MotionBlockForm',
|
||||
'AgendaUpdate',
|
||||
function($scope, MotionBlock, MotionBlockForm, AgendaUpdate) {
|
||||
// Prepare form.
|
||||
$scope.model = {};
|
||||
$scope.model.showAsAgendaItem = true;
|
||||
|
||||
// Get all form fields.
|
||||
$scope.formFields = MotionBlockForm.getFormFields();
|
||||
|
||||
// Save form.
|
||||
$scope.save = function (motionBlock) {
|
||||
MotionBlock.create(motionBlock).then(
|
||||
function (success) {
|
||||
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
|
||||
// see openslides.agenda.models.Item.ITEM_TYPE.
|
||||
var changes = [{key: 'type', value: (motionBlock.showAsAgendaItem ? 1 : 2)},
|
||||
{key: 'parent_id', value: motionBlock.agenda_parent_item_id}];
|
||||
AgendaUpdate.saveChanges(success.agenda_item_id, changes);
|
||||
$scope.closeThisDialog();
|
||||
},
|
||||
function (error) {
|
||||
var message = '';
|
||||
for (var e in error.data) {
|
||||
message += e + ': ' + error.data[e] + ' ';
|
||||
}
|
||||
$scope.alert = {type: 'danger', msg: message, show: true};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('MotionBlockUpdateCtrl', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'MotionBlock',
|
||||
'MotionBlockForm',
|
||||
'AgendaUpdate',
|
||||
'motionBlock',
|
||||
function($scope, $state, MotionBlock, MotionBlockForm, AgendaUpdate, motionBlock) {
|
||||
// TODO: Check #2486 and remove some agenda related code.
|
||||
//MotionBlock.loadRelations(motionBlock, 'agenda_item');
|
||||
$scope.alert = {};
|
||||
|
||||
// Prepare form. Set initial values by creating a deep copy of
|
||||
// motionBlock object so list/detail view is not updated while editing.
|
||||
$scope.model = angular.copy(motionBlock);
|
||||
|
||||
// Get all form fields.
|
||||
$scope.formFields = MotionBlockForm.getFormFields();
|
||||
for (var i = 0; i < $scope.formFields.length; i++) {
|
||||
if ($scope.formFields[i].key == 'showAsAgendaItem') {
|
||||
// Get state from agenda item (hidden/internal or agenda item).
|
||||
$scope.formFields[i].defaultValue = !motionBlock.agenda_item.is_hidden;
|
||||
} else if ($scope.formFields[i].key == 'agenda_parent_item_id') {
|
||||
$scope.formFields[i].defaultValue = motionBlock.agenda_item.parent_id;
|
||||
}
|
||||
}
|
||||
// Save form.
|
||||
$scope.save = function (motionBlock) {
|
||||
MotionBlock.create(motionBlock).then(
|
||||
function (success) {
|
||||
// type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item,
|
||||
// see openslides.agenda.models.Item.ITEM_TYPE.
|
||||
var changes = [{key: 'type', value: (motionBlock.showAsAgendaItem ? 1 : 2)},
|
||||
{key: 'parent_id', value: motionBlock.agenda_parent_item_id}];
|
||||
AgendaUpdate.saveChanges(success.agenda_item_id,changes);
|
||||
$scope.closeThisDialog();
|
||||
},
|
||||
function (error) {
|
||||
// Save error: revert all changes by restore
|
||||
// (refresh) original motionBlock object from server
|
||||
MotionBlock.refresh(motionBlock); // TODO: Why do we need a refresh here?
|
||||
var message = '';
|
||||
for (var e in error.data) {
|
||||
message += e + ': ' + error.data[e] + ' ';
|
||||
}
|
||||
$scope.alert = {type: 'danger', msg: message, show: true};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -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',
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
angular.module('OpenSlidesApp.motions.site', [
|
||||
'OpenSlidesApp.motions',
|
||||
'OpenSlidesApp.motions.diff',
|
||||
'OpenSlidesApp.motions.motionservices',
|
||||
'OpenSlidesApp.poll.majority',
|
||||
'OpenSlidesApp.core.pdf',
|
||||
@ -50,6 +49,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
categories: function(Category) {
|
||||
return Category.findAll();
|
||||
},
|
||||
motionBlocks: function(MotionBlock) {
|
||||
return MotionBlock.findAll();
|
||||
},
|
||||
tags: function(Tag) {
|
||||
return Tag.findAll();
|
||||
},
|
||||
@ -81,6 +83,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
categories: function(Category) {
|
||||
return Category.findAll();
|
||||
},
|
||||
motionBlocks: function(MotionBlock) {
|
||||
return MotionBlock.findAll();
|
||||
},
|
||||
users: function(User) {
|
||||
return User.findAll().catch(
|
||||
function () {
|
||||
@ -198,6 +203,66 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
},
|
||||
controller: 'CategorySortCtrl',
|
||||
templateUrl: 'static/templates/motions/category-sort.html'
|
||||
})
|
||||
// MotionBlock
|
||||
.state('motions.motionBlock', {
|
||||
url: '/blocks',
|
||||
abstract: true,
|
||||
template: '<ui-view/>',
|
||||
})
|
||||
.state('motions.motionBlock.list', {
|
||||
resolve: {
|
||||
motionBlocks: function (MotionBlock) {
|
||||
return MotionBlock.findAll();
|
||||
},
|
||||
items: function(Agenda) {
|
||||
return Agenda.findAll().catch(
|
||||
function () {
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('motions.motionBlock.detail', {
|
||||
resolve: {
|
||||
motionBlock: function(MotionBlock, $stateParams) {
|
||||
return MotionBlock.find($stateParams.id);
|
||||
},
|
||||
items: function(Agenda) {
|
||||
return Agenda.findAll().catch(
|
||||
function () {
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
// redirects to motionBlock detail and opens motionBlock edit form dialog, uses edit url,
|
||||
// used by ui-sref links from agenda only
|
||||
// (from motionBlock controller use MotionBlockForm factory instead to open dialog in front
|
||||
// of current view without redirect)
|
||||
.state('motions.motionBlock.detail.update', {
|
||||
onEnter: ['$stateParams', '$state', 'ngDialog', 'MotionBlock',
|
||||
function($stateParams, $state, ngDialog, MotionBlock) {
|
||||
ngDialog.open({
|
||||
template: 'static/templates/motions/motionBlock-form.html',
|
||||
controller: 'MotionBlockUpdateCtrl',
|
||||
className: 'ngdialog-theme-default wide-form',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
motionBlock: function () {
|
||||
return motionBlock;
|
||||
}
|
||||
},
|
||||
preCloseCallback: function() {
|
||||
$state.go('motions.motionBlock.detail', {motionBlock: $stateParams.id});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
],
|
||||
});
|
||||
}
|
||||
])
|
||||
@ -306,12 +371,13 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'Category',
|
||||
'Config',
|
||||
'Mediafile',
|
||||
'MotionBlock',
|
||||
'Tag',
|
||||
'User',
|
||||
'Workflow',
|
||||
'Agenda',
|
||||
'AgendaTree',
|
||||
function (gettextCatalog, operator, Editor, MotionComment, Category, Config, Mediafile, Tag, User, Workflow, Agenda, AgendaTree) {
|
||||
function (gettextCatalog, operator, Editor, MotionComment, Category, Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree) {
|
||||
return {
|
||||
// ngDialog for motion form
|
||||
getDialog: function (motion) {
|
||||
@ -453,6 +519,17 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
},
|
||||
{
|
||||
key: 'motion_block_id',
|
||||
type: 'select-single',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Motion block'),
|
||||
options: MotionBlock.getAll(),
|
||||
ngOptions: 'option.id as option.title for option in to.options',
|
||||
placeholder: gettextCatalog.getString('Select or search a motion block ...')
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
},
|
||||
{
|
||||
key: 'origin',
|
||||
type: 'input',
|
||||
|
@ -159,6 +159,11 @@
|
||||
<!-- Origin -->
|
||||
<h3 ng-if="motion.origin" translate>Origin</h3>
|
||||
{{ motion.origin }}
|
||||
|
||||
<!-- Motion block -->
|
||||
<h3 translate>Motion block</h3>
|
||||
{{ motion.motionBlock }}
|
||||
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h3 ng-if="motion.polls.length > 0" translate>Voting result</h3>
|
||||
|
@ -9,6 +9,10 @@
|
||||
<i class="fa fa-sitemap fa-lg"></i>
|
||||
<translate>Categories</translate>
|
||||
</a>
|
||||
<a ui-sref="motions.motionBlock.list" os-perms="motions.can_manage" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-list 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">
|
||||
<i class="fa fa-tags fa-lg"></i>
|
||||
<translate>Tags</translate>
|
||||
|
@ -0,0 +1,30 @@
|
||||
<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">
|
||||
{{ motionBlock.motions }}
|
||||
</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,49 @@
|
||||
<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 ng-click="toggleSort('name')" class="sortable">
|
||||
<translate>Name</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'name' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<tbody>
|
||||
<tr ng-repeat="motionBlock in motionBlocks | filter: filter.search | orderBy: sortColumn:reverse">
|
||||
<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 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>
|
||||
</table>
|
||||
</div>
|
@ -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>
|
@ -24,6 +24,7 @@ from openslides.utils.views import APIView, PDFView, SingleObjectMixin
|
||||
from .access_permissions import (
|
||||
CategoryAccessPermissions,
|
||||
MotionAccessPermissions,
|
||||
MotionBlockAccessPermissions,
|
||||
MotionChangeRecommendationAccessPermissions,
|
||||
WorkflowAccessPermissions,
|
||||
)
|
||||
@ -31,6 +32,7 @@ from .exceptions import WorkflowError
|
||||
from .models import (
|
||||
Category,
|
||||
Motion,
|
||||
MotionBlock,
|
||||
MotionChangeRecommendation,
|
||||
MotionPoll,
|
||||
MotionVersion,
|
||||
@ -89,6 +91,8 @@ class MotionViewSet(ModelViewSet):
|
||||
# Non-staff users are not allowed to send submitter or supporter data.
|
||||
self.permission_denied(request)
|
||||
|
||||
# TODO: Should non staff users be allowed to set motions to blocks or send categories, ...? #2506
|
||||
|
||||
# Check permission to send comment data.
|
||||
if not request.user.has_perm('motions.can_see_and_manage_comments'):
|
||||
try:
|
||||
@ -373,7 +377,7 @@ class CategoryViewSet(ModelViewSet):
|
||||
API endpoint for categories.
|
||||
|
||||
There are the following views: metadata, list, retrieve, create,
|
||||
partial_update, update and destroy.
|
||||
partial_update, update, destroy and numbering.
|
||||
"""
|
||||
access_permissions = CategoryAccessPermissions()
|
||||
queryset = Category.objects.all()
|
||||
@ -445,6 +449,32 @@ class CategoryViewSet(ModelViewSet):
|
||||
return response
|
||||
|
||||
|
||||
class MotionBlockViewSet(ModelViewSet):
|
||||
"""
|
||||
API endpoint for motion blocks.
|
||||
|
||||
There are the following views: metadata, list, retrieve, create,
|
||||
partial_update, update and destroy.
|
||||
"""
|
||||
access_permissions = MotionBlockAccessPermissions()
|
||||
queryset = MotionBlock.objects.all()
|
||||
|
||||
def check_view_permissions(self):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action == 'metadata':
|
||||
result = self.request.user.has_perm('motions.can_see')
|
||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||
result = (self.request.user.has_perm('motions.can_see') and
|
||||
self.request.user.has_perm('motions.can_manage'))
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
|
||||
|
||||
class WorkflowViewSet(ModelViewSet):
|
||||
"""
|
||||
API endpoint for workflows.
|
||||
|
Loading…
Reference in New Issue
Block a user