Merge pull request #1663 from emanuelschuetze/motions-template2
Work on motions app.
This commit is contained in:
commit
2417549755
39
bower.json
39
bower.json
@ -2,29 +2,30 @@
|
||||
"name": "OpenSlides",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"lodash": "~3.0.1",
|
||||
"lodash": "~3.10.0",
|
||||
"jquery": "~2.1.4",
|
||||
"jquery.cookie": "~1.4.1",
|
||||
"bootstrap-css-only": "~3.3.4",
|
||||
"angular": "~1.3.15",
|
||||
"angular-bootstrap": "~0.13.0",
|
||||
"angular-messages": "~1.3.15",
|
||||
"angular-animate": "~1.3.15",
|
||||
"angular-csv-import": "~0.0.15",
|
||||
"angular-loading-bar": "~0.7.1",
|
||||
"angular-ui-router": "~0.2.13",
|
||||
"angular-ui-select": "~0.12",
|
||||
"angular-ui-switch": "~0.1.0",
|
||||
"angular-ui-tree": "~2.2.0",
|
||||
"angular-gettext": "~2.0.2",
|
||||
"angular-sanitize": "~1.3.15",
|
||||
"bootstrap-css-only": "~3.3.5",
|
||||
"angular": "~1.4.7",
|
||||
"angular-messages": "~1.4.7",
|
||||
"angular-animate": "~1.4.7",
|
||||
"angular-sanitize": "~1.4.7",
|
||||
"angular-bootstrap": "~0.14.3",
|
||||
"angular-csv-import": "~0.0.26",
|
||||
"angular-formly-templates-bootstrap": "~6.1.5",
|
||||
"angular-formly": "~7.3.2",
|
||||
"angular-loading-bar": "~0.8.0",
|
||||
"angular-ui-router": "~0.2.15",
|
||||
"angular-ui-select": "~0.13.1",
|
||||
"angular-ui-switch": "~0.1.1",
|
||||
"angular-ui-tree": "~2.10.0",
|
||||
"angular-gettext": "~2.1.2",
|
||||
"angular-xeditable": "~0.1.9",
|
||||
"ng-fab-form": "~1.2.7",
|
||||
"ngBootbox": "~0.0.5",
|
||||
"ngBootbox": "~0.1.2",
|
||||
"sockjs": "~0.3.4",
|
||||
"font-awesome-bower": "4.3.0",
|
||||
"js-data": "~2.3.0",
|
||||
"js-data-angular": "~3.0.0",
|
||||
"font-awesome-bower": "~4.4.0",
|
||||
"js-data": "~2.8.1",
|
||||
"js-data-angular": "~3.1.0",
|
||||
"ng-file-upload": "~9.1.2"
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- projector column -->
|
||||
<th ng-show="!isDeleteMode" os-perms="core.can_manage_projector" class="firstColumn">
|
||||
<th ng-show="!isDeleteMode" os-perms="core.can_manage_projector" class="firstColumn"></th>
|
||||
<!-- delete selection column -->
|
||||
<th ng-show="isDeleteMode" os-perms-lite="agenda.can_manage" class="firstColumn deleteColumn"
|
||||
ng-click="$event.stopPropagation();">
|
||||
|
@ -79,7 +79,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
|
||||
|
||||
.controller('AssignmentDetailCtrl', function($scope, Assignment, assignment) {
|
||||
Assignment.bindOne(assignment.id, $scope, 'assignment');
|
||||
Assignment.loadRelations(assignment);
|
||||
Assignment.loadRelations(assignment, 'agenda_item');
|
||||
})
|
||||
|
||||
.controller('AssignmentCreateCtrl', function($scope, $state, Assignment) {
|
||||
|
@ -49,83 +49,49 @@ body {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
.spacer, .spacer-top {
|
||||
margin-top: 7px;
|
||||
}
|
||||
.spacer-right {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.hoverActions {
|
||||
font-size: 85%;
|
||||
}
|
||||
.hiddenDiv {
|
||||
visibility: hidden;
|
||||
}
|
||||
/* override bootstraps's progress bar for poll results*/
|
||||
.progress {
|
||||
height: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* voting results */
|
||||
.result_label {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* TODO: used by ng-fab-forms */
|
||||
.validation-success {
|
||||
opacity: 1;
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -7.2px;
|
||||
bottom: -7.2px;
|
||||
font-size: 28.8px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
border-radius: 36px;
|
||||
color: #62b14c;
|
||||
transition: all ease-out 0.32s; }
|
||||
.validation-success:after {
|
||||
display: block;
|
||||
content: '\e013';
|
||||
font-family: 'Glyphicons Halflings'; }
|
||||
.validation-success.ng-hide {
|
||||
transition-delay: 0s;
|
||||
transition: all ease-out 0.12s;
|
||||
opacity: 0;
|
||||
transform: rotate(360deg); }
|
||||
|
||||
.ng-hide-remove li {
|
||||
opacity: 0; }
|
||||
/* ngAnimate classes */
|
||||
.animate-item.ng-enter {
|
||||
-webkit-animation: fade-in 1s linear;
|
||||
animation: fade-in 1.5s linear;
|
||||
}
|
||||
.animate-item.ng-leave {
|
||||
-webkit-animation: fade-out 1s linear;
|
||||
animation: fade-out 1.5s linear;
|
||||
}
|
||||
|
||||
.validation {
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
overflow: visible;
|
||||
background: #c00640; }
|
||||
.validation ul {
|
||||
display: block;
|
||||
overflow: hidden; }
|
||||
.validation li {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
background: #c00640;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: -10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
padding: 2px 10px;
|
||||
color: #fff;
|
||||
transform: rotate(0deg);
|
||||
transition: all ease-in 0.2s;
|
||||
opacity: 1;
|
||||
transition-delay: 0s; }
|
||||
.validation li.ng-enter {
|
||||
opacity: 0;
|
||||
top: 0; }
|
||||
.validation li.ng-leave {
|
||||
transition: all ease-in 0s;
|
||||
opacity: 0; }
|
||||
*:focus + .validation li {
|
||||
background-color: #63bff8 !important; }
|
||||
|
||||
input.ng-touched.ng-invalid:not(.ng-valid), textarea.ng-touched.ng-invalid:not(.ng-valid), select.ng-touched.ng-invalid:not(.ng-valid) {
|
||||
border-color: #c00640; }
|
||||
input:focus, input:focus.ng-touched.ng-invalid:not(.ng-valid), textarea:focus, textarea:focus.ng-touched.ng-invalid:not(.ng-valid), select:focus, select:focus.ng-touched.ng-invalid:not(.ng-valid) {
|
||||
border-color: #63bff8; }
|
||||
input.ng-valid-required.ng-valid:not(.ng-invalid), textarea.ng-valid-required.ng-valid:not(.ng-invalid), select.ng-valid-required.ng-valid:not(.ng-invalid) {
|
||||
border-color: #62b14c; }
|
||||
|
||||
form[class*="ng-invalid"] button.btn[type=submit] {
|
||||
background: #63bff8;
|
||||
transition: none; }
|
||||
|
||||
form button.btn[type=submit] {
|
||||
transition: all ease-in 0.5s;
|
||||
background: #62b14c; }
|
||||
@keyframes fade-out {
|
||||
0% { opacity: 1; background: none; }
|
||||
25% { opacity: 1; background: #f8efc0; }
|
||||
100% { opacity: 0; background: none; }
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; background: none; }
|
||||
25% { opacity: 1; background: #dff0d8; }
|
||||
100% { opacity: 1; background: none; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -274,14 +240,25 @@ tr.offline td, li.offline {
|
||||
tr.activeline td, li.activeline, .projected {
|
||||
background-color: #bed4de;
|
||||
}
|
||||
tr.selected td {
|
||||
background-color: #ff9999;
|
||||
}
|
||||
.nopadding {
|
||||
padding: 0;
|
||||
}
|
||||
.alert form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
tr.total td {
|
||||
border-top: 1px solid #333333;
|
||||
.slimlist {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.smallhr {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
.resultcolumn {
|
||||
font-weight: bold;
|
||||
}
|
||||
.nobr {
|
||||
white-space: nowrap;
|
||||
|
@ -111,7 +111,7 @@ angular.module('OpenSlidesApp.core', [
|
||||
// Load the global data on startup
|
||||
.run([
|
||||
'loadGlobalData',
|
||||
function(loadGlobalData, operator) {
|
||||
function(loadGlobalData) {
|
||||
loadGlobalData();
|
||||
}
|
||||
])
|
||||
|
@ -6,8 +6,9 @@
|
||||
angular.module('OpenSlidesApp.core.site', [
|
||||
'OpenSlidesApp.core',
|
||||
'ui.router',
|
||||
'formly',
|
||||
'formlyBootstrap',
|
||||
'ngBootbox',
|
||||
'ngFabForm',
|
||||
'ngMessages',
|
||||
'ngCsvImport',
|
||||
'ngSanitize', // TODO: only use this in functions that need it.
|
||||
@ -260,13 +261,6 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
$locationProvider.html5Mode(true);
|
||||
})
|
||||
|
||||
// config for ng-fab-form
|
||||
.config(function(ngFabFormProvider) {
|
||||
ngFabFormProvider.extendConfig({
|
||||
setAsteriskForRequiredLabel: true
|
||||
});
|
||||
})
|
||||
|
||||
// Helper to add ui.router states at runtime.
|
||||
// Needed for the django url_patterns.
|
||||
.provider('runtimeStates', function($stateProvider) {
|
||||
@ -299,6 +293,26 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
editableOptions.theme = 'bs3';
|
||||
})
|
||||
|
||||
// angular formly config options
|
||||
.run([
|
||||
'formlyConfig',
|
||||
function (formlyConfig) {
|
||||
// NOTE: This next line is highly recommended. Otherwise Chrome's autocomplete will appear over your options!
|
||||
formlyConfig.extras.removeChromeAutoComplete = true;
|
||||
|
||||
// Configure custom types
|
||||
formlyConfig.setType({
|
||||
name: 'ui-select-single',
|
||||
extends: 'select',
|
||||
templateUrl: 'static/templates/core/ui-select-single.html'
|
||||
});
|
||||
formlyConfig.setType({
|
||||
name: 'ui-select-multiple',
|
||||
extends: 'select',
|
||||
templateUrl: 'static/templates/core/ui-select-multiple.html'
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
// html-tag os-form-field to generate generic from fields
|
||||
// TODO: make it possible to use other fields then config fields
|
||||
@ -646,7 +660,7 @@ angular.module('OpenSlidesApp.core.site', [
|
||||
|
||||
.controller('CustomslideDetailCtrl', function($scope, Customslide, customslide) {
|
||||
Customslide.bindOne(customslide.id, $scope, 'customslide');
|
||||
Customslide.loadRelations(customslide);
|
||||
Customslide.loadRelations(customslide, 'agenda_item');
|
||||
})
|
||||
|
||||
.controller('CustomslideCreateCtrl', function($scope, $state, Customslide) {
|
||||
|
@ -0,0 +1,10 @@
|
||||
<!-- custom angular formly template for ui-select multiple form field -->
|
||||
<ui-select multiple data-ng-model="model[options.key]" data-required="{{to.required}}"
|
||||
data-disabled="{{to.disabled}}" theme="bootstrap">
|
||||
<ui-select-match placeholder="{{to.placeholder}}">
|
||||
{{$item[to.labelProp]}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices data-repeat="{{to.ngOptions}}">
|
||||
<div ng-bind-html="option[to.labelProp] | highlight: $select.search"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
10
openslides/core/static/templates/core/ui-select-single.html
Normal file
10
openslides/core/static/templates/core/ui-select-single.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!-- custom angular formly template for ui-select single form field -->
|
||||
<ui-select data-ng-model="model[options.key]" data-required="{{to.required}}"
|
||||
data-disabled="{{to.disabled}}" theme="bootstrap">
|
||||
<ui-select-match placeholder="{{to.placeholder}}" data-allow-clear="true">
|
||||
{{$select.selected[to.labelProp]}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices data-repeat="{{to.ngOptions}}">
|
||||
<div ng-bind-html="option[to.labelProp] | highlight: $select.search"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
29
openslides/motions/migrations/0004_auto_20151105_2312.py
Normal file
29
openslides/motions/migrations/0004_auto_20151105_2312.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0003_auto_20150904_2029'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='state',
|
||||
old_name='icon',
|
||||
new_name='css_class',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='state',
|
||||
name='css_class',
|
||||
field=models.CharField(max_length=255, default='primary'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='state',
|
||||
name='workflow',
|
||||
field=models.ForeignKey(to='motions.Workflow', related_name='states'),
|
||||
),
|
||||
]
|
@ -409,6 +409,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Returns the id of the workflow of the motion.
|
||||
"""
|
||||
# TODO: Rename to workflow_id
|
||||
return self.state.workflow.pk
|
||||
|
||||
def set_state(self, state):
|
||||
@ -442,7 +443,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
new_state = self.state.workflow.first_state
|
||||
else:
|
||||
new_state = (Workflow.objects.get(pk=config['motions_workflow']).first_state or
|
||||
Workflow.objects.get(pk=config['motions_workflow']).state_set.all()[0])
|
||||
Workflow.objects.get(pk=config['motions_workflow']).states.all()[0])
|
||||
self.set_state(new_state)
|
||||
|
||||
def get_agenda_title(self):
|
||||
@ -729,14 +730,18 @@ class State(RESTModelMixin, models.Model):
|
||||
action_word = models.CharField(max_length=255)
|
||||
"""An alternative string to be used for a button to switch to this state."""
|
||||
|
||||
workflow = models.ForeignKey('Workflow')
|
||||
workflow = models.ForeignKey('Workflow', related_name='states')
|
||||
"""A many-to-one relation to a workflow."""
|
||||
|
||||
next_states = models.ManyToManyField('self', symmetrical=False)
|
||||
"""A many-to-many relation to all states, that can be choosen from this state."""
|
||||
|
||||
icon = models.CharField(max_length=255)
|
||||
"""A string representing the url to the icon-image."""
|
||||
css_class = models.CharField(max_length=255, default='primary')
|
||||
"""
|
||||
A css class string for showing the state name in a coloured label based on bootstrap,
|
||||
e.g. 'danger' (red), 'success' (green), 'warning' (yellow), 'default' (grey).
|
||||
Default value is 'primary' (blue).
|
||||
"""
|
||||
|
||||
required_permission_to_see = models.CharField(max_length=255, blank=True)
|
||||
"""
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django.db import transaction
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openslides.core.config import config
|
||||
from openslides.utils.rest_api import (
|
||||
CharField,
|
||||
DictField,
|
||||
@ -50,7 +49,7 @@ class StateSerializer(ModelSerializer):
|
||||
'id',
|
||||
'name',
|
||||
'action_word',
|
||||
'icon',
|
||||
'css_class',
|
||||
'required_permission_to_see',
|
||||
'allow_support',
|
||||
'allow_create_poll',
|
||||
@ -58,28 +57,37 @@ class StateSerializer(ModelSerializer):
|
||||
'versioning',
|
||||
'leave_old_version_active',
|
||||
'dont_set_identifier',
|
||||
'next_states',)
|
||||
'next_states',
|
||||
'workflow')
|
||||
|
||||
|
||||
class WorkflowSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Workflow objects.
|
||||
"""
|
||||
state_set = StateSerializer(many=True, read_only=True)
|
||||
states = StateSerializer(many=True, read_only=True)
|
||||
first_state = PrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = ('id', 'name', 'state_set', 'first_state',)
|
||||
fields = ('id', 'name', 'states', 'first_state',)
|
||||
|
||||
|
||||
class MotionLogSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionLog objects.
|
||||
"""
|
||||
message = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = MotionLog
|
||||
fields = ('message_list', 'person', 'time',)
|
||||
fields = ('message_list', 'person', 'time', 'message',)
|
||||
|
||||
def get_message(self, obj):
|
||||
"""
|
||||
Concats the message parts to one string. Useful for smart template code.
|
||||
"""
|
||||
return str(obj)
|
||||
|
||||
|
||||
class MotionPollSerializer(ModelSerializer):
|
||||
@ -97,6 +105,7 @@ class MotionPollSerializer(ModelSerializer):
|
||||
model = MotionPoll
|
||||
fields = (
|
||||
'id',
|
||||
'motion',
|
||||
'yes',
|
||||
'no',
|
||||
'abstain',
|
||||
@ -179,11 +188,14 @@ class MotionSerializer(ModelSerializer):
|
||||
log_messages = MotionLogSerializer(many=True, read_only=True)
|
||||
polls = MotionPollSerializer(many=True, read_only=True)
|
||||
reason = CharField(allow_blank=True, required=False, write_only=True)
|
||||
state = StateSerializer(read_only=True)
|
||||
text = CharField(write_only=True)
|
||||
title = CharField(max_length=255, write_only=True)
|
||||
versions = MotionVersionSerializer(many=True, read_only=True)
|
||||
workflow = IntegerField(min_value=1, required=False, validators=[validate_workflow_field])
|
||||
workflow_id = IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
validators=[validate_workflow_field],
|
||||
write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Motion
|
||||
@ -200,13 +212,13 @@ class MotionSerializer(ModelSerializer):
|
||||
'submitters',
|
||||
'supporters',
|
||||
'state',
|
||||
'workflow',
|
||||
'workflow_id',
|
||||
'tags',
|
||||
'attachments',
|
||||
'polls',
|
||||
'agenda_item_id',
|
||||
'log_messages',)
|
||||
read_only_fields = ('parent',) # Some other fields are also read_only. See definitions above.
|
||||
read_only_fields = ('parent', 'state') # Some other fields are also read_only. See definitions above.
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
@ -219,7 +231,7 @@ class MotionSerializer(ModelSerializer):
|
||||
motion.reason = validated_data.get('reason', '')
|
||||
motion.identifier = validated_data.get('identifier')
|
||||
motion.category = validated_data.get('category')
|
||||
motion.reset_state(validated_data.get('workflow', int(config['motions_workflow'])))
|
||||
motion.reset_state(validated_data.get('workflow_id'))
|
||||
motion.save()
|
||||
if validated_data.get('submitters'):
|
||||
motion.submitters.add(*validated_data['submitters'])
|
||||
@ -241,9 +253,9 @@ class MotionSerializer(ModelSerializer):
|
||||
setattr(motion, key, validated_data[key])
|
||||
|
||||
# Workflow.
|
||||
workflow = validated_data.get('workflow')
|
||||
if workflow is not None and workflow != motion.workflow:
|
||||
motion.reset_state(workflow)
|
||||
workflow_id = validated_data.get('workflow_id')
|
||||
if workflow_id is not None and workflow_id != motion.workflow:
|
||||
motion.reset_state(workflow_id)
|
||||
|
||||
# Decide if a new version is saved to the database.
|
||||
if (motion.state.versioning and
|
||||
|
@ -189,13 +189,16 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
allow_submitter_edit=True)
|
||||
state_1_2 = State.objects.create(name=ugettext_noop('accepted'),
|
||||
workflow=workflow_1,
|
||||
action_word=ugettext_noop('Accept'))
|
||||
action_word=ugettext_noop('Accept'),
|
||||
css_class='success')
|
||||
state_1_3 = State.objects.create(name=ugettext_noop('rejected'),
|
||||
workflow=workflow_1,
|
||||
action_word=ugettext_noop('Reject'))
|
||||
action_word=ugettext_noop('Reject'),
|
||||
css_class='danger')
|
||||
state_1_4 = State.objects.create(name=ugettext_noop('not decided'),
|
||||
workflow=workflow_1,
|
||||
action_word=ugettext_noop('Do not decide'))
|
||||
action_word=ugettext_noop('Do not decide'),
|
||||
css_class='default')
|
||||
state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
|
||||
workflow_1.first_state = state_1_1
|
||||
workflow_1.save()
|
||||
@ -216,35 +219,43 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
state_2_3 = State.objects.create(name=ugettext_noop('accepted'),
|
||||
workflow=workflow_2,
|
||||
action_word=ugettext_noop('Accept'),
|
||||
versioning=True)
|
||||
versioning=True,
|
||||
css_class='success')
|
||||
state_2_4 = State.objects.create(name=ugettext_noop('rejected'),
|
||||
workflow=workflow_2,
|
||||
action_word=ugettext_noop('Reject'),
|
||||
versioning=True)
|
||||
versioning=True,
|
||||
css_class='danger')
|
||||
state_2_5 = State.objects.create(name=ugettext_noop('withdrawed'),
|
||||
workflow=workflow_2,
|
||||
action_word=ugettext_noop('Withdraw'),
|
||||
versioning=True)
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_6 = State.objects.create(name=ugettext_noop('adjourned'),
|
||||
workflow=workflow_2,
|
||||
action_word=ugettext_noop('Adjourn'),
|
||||
versioning=True)
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_7 = State.objects.create(name=ugettext_noop('not concerned'),
|
||||
workflow=workflow_2,
|
||||
action_word=ugettext_noop('Do not concern'),
|
||||
versioning=True)
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_8 = State.objects.create(name=ugettext_noop('commited a bill'),
|
||||
workflow=workflow_2,
|
||||
action_word=ugettext_noop('Commit a bill'),
|
||||
versioning=True)
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_9 = State.objects.create(name=ugettext_noop('needs review'),
|
||||
workflow=workflow_2,
|
||||
action_word=ugettext_noop('Needs review'),
|
||||
versioning=True)
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_10 = State.objects.create(name=ugettext_noop('rejected (not authorized)'),
|
||||
workflow=workflow_2,
|
||||
action_word=ugettext_noop('Reject (not authorized)'),
|
||||
versioning=True)
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_1.next_states.add(state_2_2, state_2_5, state_2_10)
|
||||
state_2_2.next_states.add(state_2_3, state_2_4, state_2_5, state_2_6, state_2_7, state_2_8, state_2_9)
|
||||
workflow_2.first_state = state_2_1
|
||||
|
@ -2,10 +2,163 @@
|
||||
|
||||
angular.module('OpenSlidesApp.motions', [])
|
||||
|
||||
.factory('Motion', [
|
||||
.factory('WorkflowState', [
|
||||
'DS',
|
||||
function (DS) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/workflowstate',
|
||||
methods: {
|
||||
getNextStates: function () {
|
||||
var states = [];
|
||||
_.forEach(this.next_states_id, function (stateId) {
|
||||
states.push(DS.get('motions/workflowstate', stateId));
|
||||
})
|
||||
return states;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
])
|
||||
|
||||
.factory('Workflow', [
|
||||
'DS',
|
||||
'jsDataModel',
|
||||
function(DS, jsDataModel) {
|
||||
'WorkflowState',
|
||||
function (DS, jsDataModel, WorkflowState) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/workflow',
|
||||
useClass: jsDataModel,
|
||||
relations: {
|
||||
hasMany: {
|
||||
'motions/workflowstate': {
|
||||
localField: 'states',
|
||||
foreignKey: 'workflow_id',
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
])
|
||||
|
||||
// Load all MotionWorkflows at startup
|
||||
.run([
|
||||
'Workflow',
|
||||
function (Workflow) {
|
||||
Workflow.findAll();
|
||||
}
|
||||
])
|
||||
|
||||
.factory('MotionPoll', [
|
||||
'DS',
|
||||
'Config',
|
||||
function (DS, Config) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/motionpoll',
|
||||
relations: {
|
||||
belongsTo: {
|
||||
'motions/motion': {
|
||||
localField: 'motion',
|
||||
localKey: 'motion_id',
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getYesPercent: function (valueOnly) {
|
||||
var config = Config.get('motions_poll_100_percent_base').value;
|
||||
var returnvalue;
|
||||
if (config == "WITHOUT_INVALID" && this.votesvalid > 0 && this.yes >= 0) {
|
||||
returnvalue = Math.round(this.yes * 100 / this.votesvalid * 10) / 10;
|
||||
} else if (config == "WITH_INVALID" && this.votescast > 0 && this.yes >= 0) {
|
||||
returnvalue = Math.round(this.yes * 100 / (this.votescast) * 10) / 10;
|
||||
} else {
|
||||
returnvalue = null;
|
||||
}
|
||||
if (!valueOnly && returnvalue != null) {
|
||||
returnvalue = "(" + returnvalue + "%)";
|
||||
}
|
||||
return returnvalue;
|
||||
},
|
||||
getNoPercent: function (valueOnly) {
|
||||
var config = Config.get('motions_poll_100_percent_base').value;
|
||||
var returnvalue;
|
||||
if (config == "WITHOUT_INVALID" && this.votesvalid > 0 && this.no >= 0) {
|
||||
returnvalue = Math.round(this.no * 100 / this.votesvalid * 10) / 10;
|
||||
} else if (config == "WITH_INVALID" && this.votescast > 0 && this.no >= 0) {
|
||||
returnvalue = Math.round(this.no * 100 / (this.votescast) * 10) / 10;
|
||||
} else {
|
||||
returnvalue = null;
|
||||
}
|
||||
if (!valueOnly && returnvalue != null) {
|
||||
returnvalue = "(" + returnvalue + "%)";
|
||||
}
|
||||
return returnvalue;
|
||||
},
|
||||
getAbstainPercent: function (valueOnly) {
|
||||
var config = Config.get('motions_poll_100_percent_base').value;
|
||||
var returnvalue;
|
||||
if (config == "WITHOUT_INVALID" && this.votesvalid > 0 && this.abstain >= 0) {
|
||||
returnvalue = Math.round(this.abstain * 100 / this.votesvalid * 10) / 10;
|
||||
} else if (config == "WITH_INVALID" && this.votescast > 0 && this.abstain >= 0) {
|
||||
returnvalue = Math.round(this.abstain * 100 / (this.votescast) * 10) / 10;
|
||||
} else {
|
||||
returnvalue = null;
|
||||
}
|
||||
if (!valueOnly && returnvalue != null) {
|
||||
returnvalue = "(" + returnvalue + "%)";
|
||||
}
|
||||
return returnvalue;
|
||||
},
|
||||
getVotesValidPercent: function (valueOnly) {
|
||||
var config = Config.get('motions_poll_100_percent_base').value;
|
||||
var returnvalue;
|
||||
if (config == "WITHOUT_INVALID" && this.votevalid >= 0) {
|
||||
returnvalue = 100;
|
||||
} else if (config == "WITH_INVALID" && this.votevalid >= 0) {
|
||||
returnvalue = Math.round(this.votesvalid * 100 / (this.votescast) * 10) / 10;
|
||||
} else {
|
||||
returnvalue = null;
|
||||
}
|
||||
if (!valueOnly && returnvalue != null) {
|
||||
returnvalue = "(" + returnvalue + "%)";
|
||||
}
|
||||
return returnvalue;
|
||||
},
|
||||
getVotesInvalidPercent: function (valueOnly) {
|
||||
var config = Config.get('motions_poll_100_percent_base').value;
|
||||
var returnvalue;
|
||||
if (config == "WITH_INVALID" && this.voteinvalid >= 0) {
|
||||
returnvalue = Math.round(this.votesinvalid * 100 / (this.votescast) * 10) / 10;
|
||||
} else {
|
||||
returnvalue = null;
|
||||
}
|
||||
if (!valueOnly && returnvalue != null) {
|
||||
returnvalue = "(" + returnvalue + "%)";
|
||||
}
|
||||
return returnvalue;
|
||||
},
|
||||
getVotesCastPercent: function (valueOnly) {
|
||||
var config = Config.get('motions_poll_100_percent_base').value;
|
||||
var returnvalue;
|
||||
if (config == "WITH_INVALID" && this.votecast >= 0) {
|
||||
returnvalue = 100;
|
||||
} else {
|
||||
returnvalue = null;
|
||||
}
|
||||
if (!valueOnly && returnvalue != null) {
|
||||
returnvalue = "(" + returnvalue + "%)";
|
||||
}
|
||||
return returnvalue;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.factory('Motion', [
|
||||
'DS',
|
||||
'MotionPoll',
|
||||
'jsDataModel',
|
||||
function(DS, MotionPoll, jsDataModel) {
|
||||
var name = 'motions/motion'
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
@ -60,6 +213,10 @@ angular.module('OpenSlidesApp.motions', [])
|
||||
localField: 'tags',
|
||||
localKeys: 'tags_id',
|
||||
},
|
||||
'mediafiles/mediafile': {
|
||||
localField: 'attachments',
|
||||
localKeys: 'attachments_id',
|
||||
},
|
||||
'users/user': [
|
||||
{
|
||||
localField: 'submitters',
|
||||
@ -70,25 +227,167 @@ angular.module('OpenSlidesApp.motions', [])
|
||||
localKeys: 'supporters_id',
|
||||
}
|
||||
],
|
||||
'motions/motionpoll': {
|
||||
localField: 'polls',
|
||||
foreignKey: 'motion_id',
|
||||
}
|
||||
},
|
||||
hasOne: {
|
||||
'motions/workflowstate': {
|
||||
localField: 'state',
|
||||
localKey: 'state_id',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
// Provide generic motion form fields for create and update view
|
||||
.factory('MotionFormFieldFactory', [
|
||||
'gettext',
|
||||
'Category',
|
||||
'Config',
|
||||
'Mediafile',
|
||||
'Tag',
|
||||
'User',
|
||||
'Workflow',
|
||||
function (gettext, Category, Config, Mediafile, Tag, User, Workflow) {
|
||||
return {
|
||||
getFormFields: function () {
|
||||
return [
|
||||
{
|
||||
key: 'identifier',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettext('Identifier')
|
||||
},
|
||||
hide: true
|
||||
},
|
||||
{
|
||||
key: 'submitters_id',
|
||||
type: 'ui-select-multiple',
|
||||
templateOptions: {
|
||||
label: gettext('Submitters'),
|
||||
optionsAttr: 'bs-options',
|
||||
options: User.getAll(),
|
||||
ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',
|
||||
valueProp: 'id',
|
||||
labelProp: 'full_name',
|
||||
placeholder: gettext('Select or search a submitter...')
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettext('Title'),
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'text',
|
||||
type: 'textarea',
|
||||
templateOptions: {
|
||||
label: gettext('Text'),
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'reason',
|
||||
type: 'textarea',
|
||||
templateOptions: {
|
||||
label: gettext('Reason')
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'more',
|
||||
type: 'checkbox',
|
||||
templateOptions: {
|
||||
label: gettext('Show extended fields')
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'attachments_id',
|
||||
type: 'ui-select-multiple',
|
||||
templateOptions: {
|
||||
label: gettext('Attachment'),
|
||||
optionsAttr: 'bs-options',
|
||||
options: Mediafile.getAll(),
|
||||
ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',
|
||||
valueProp: 'id',
|
||||
labelProp: 'title_or_filename',
|
||||
placeholder: gettext('Select or search an attachment...')
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
},
|
||||
{
|
||||
key: 'category_id',
|
||||
type: 'ui-select-single',
|
||||
templateOptions: {
|
||||
label: gettext('Category'),
|
||||
optionsAttr: 'bs-options',
|
||||
options: Category.getAll(),
|
||||
ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',
|
||||
valueProp: 'id',
|
||||
labelProp: 'name',
|
||||
placeholder: gettext('Select or search a category...')
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
},
|
||||
{
|
||||
key: 'tags_id',
|
||||
type: 'ui-select-multiple',
|
||||
templateOptions: {
|
||||
label: gettext('Tags'),
|
||||
optionsAttr: 'bs-options',
|
||||
options: Tag.getAll(),
|
||||
ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',
|
||||
valueProp: 'id',
|
||||
labelProp: 'name',
|
||||
placeholder: gettext('Select or search a tag...')
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
},
|
||||
{
|
||||
key: 'supporters_id',
|
||||
type: 'ui-select-multiple',
|
||||
templateOptions: {
|
||||
label: gettext('Supporters'),
|
||||
optionsAttr: 'bs-options',
|
||||
options: User.getAll(),
|
||||
ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',
|
||||
valueProp: 'id',
|
||||
labelProp: 'full_name',
|
||||
placeholder: gettext('Select or search a supporter...')
|
||||
},
|
||||
hideExpression: '!model.more'
|
||||
},
|
||||
{
|
||||
key: 'workflow_id',
|
||||
type: 'ui-select-single',
|
||||
templateOptions: {
|
||||
label: gettext('Workflow'),
|
||||
optionsAttr: 'bs-options',
|
||||
options: Workflow.getAll(),
|
||||
ngOptions: 'option[to.valueProp] as option in to.options | filter: $select.search',
|
||||
valueProp: 'id',
|
||||
labelProp: 'name',
|
||||
placeholder: gettext('Select or search a workflow...')
|
||||
},
|
||||
hideExpression: '!model.more',
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
.factory('Category', ['DS', function(DS) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/category',
|
||||
});
|
||||
}])
|
||||
|
||||
.factory('Workflow', ['DS', function(DS) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/workflow',
|
||||
});
|
||||
}])
|
||||
|
||||
.run(['Motion', 'Category', 'Workflow', function(Motion, Category, Workflow) {}]);
|
||||
.run(['Motion', 'Category', function(Motion, Category) {}]);
|
||||
|
||||
|
||||
angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||
@ -125,6 +424,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||
categories: function(Category) {
|
||||
return Category.findAll();
|
||||
},
|
||||
tags: function(Tag) {
|
||||
return Tag.findAll();
|
||||
},
|
||||
users: function(User) {
|
||||
return User.findAll();
|
||||
}
|
||||
@ -132,15 +434,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||
})
|
||||
.state('motions.motion.create', {
|
||||
resolve: {
|
||||
items: function(Agenda) {
|
||||
return Agenda.findAll();
|
||||
},
|
||||
categories: function(Category) {
|
||||
return Category.findAll();
|
||||
},
|
||||
workflows: function(Workflow) {
|
||||
return Workflow.findAll();
|
||||
},
|
||||
tags: function(Tag) {
|
||||
return Tag.findAll();
|
||||
},
|
||||
@ -149,6 +445,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||
},
|
||||
mediafiles: function(Mediafile) {
|
||||
return Mediafile.findAll();
|
||||
},
|
||||
workflows: function(Workflow) {
|
||||
return Workflow.findAll();
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -163,9 +462,12 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||
users: function(User) {
|
||||
return User.findAll();
|
||||
},
|
||||
mediafiles: function(Mediafile) {
|
||||
return Mediafile.findAll();
|
||||
},
|
||||
tags: function(Tag) {
|
||||
return Tag.findAll();
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('motions.motion.detail.update', {
|
||||
@ -173,15 +475,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||
'@motions.motion': {}
|
||||
},
|
||||
resolve: {
|
||||
items: function(Agenda) {
|
||||
return Agenda.findAll();
|
||||
},
|
||||
categories: function(Category) {
|
||||
return Category.findAll();
|
||||
},
|
||||
workflows: function(Workflow) {
|
||||
return Workflow.findAll();
|
||||
},
|
||||
tags: function(Tag) {
|
||||
return Tag.findAll();
|
||||
},
|
||||
@ -190,6 +486,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||
},
|
||||
mediafiles: function(Mediafile) {
|
||||
return Mediafile.findAll();
|
||||
},
|
||||
workflows: function(Workflow) {
|
||||
return Workflow.findAll();
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -225,92 +524,283 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
|
||||
})
|
||||
})
|
||||
|
||||
.controller('MotionListCtrl', function($scope, Motion, Category, User) {
|
||||
Motion.bindAll({}, $scope, 'motions');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
|
||||
// setup table sorting
|
||||
$scope.sortColumn = 'identifier';
|
||||
$scope.filterPresent = '';
|
||||
$scope.reverse = false;
|
||||
// function to sort by clicked column
|
||||
$scope.toggleSort = function ( column ) {
|
||||
if ( $scope.sortColumn === column ) {
|
||||
$scope.reverse = !$scope.reverse;
|
||||
}
|
||||
$scope.sortColumn = column;
|
||||
};
|
||||
|
||||
// save changed motion
|
||||
$scope.save = function (motion) {
|
||||
Motion.save(motion);
|
||||
};
|
||||
// delete selected motion
|
||||
$scope.delete = function (motion) {
|
||||
Motion.destroy(motion.id);
|
||||
};
|
||||
})
|
||||
|
||||
.controller('MotionDetailCtrl', function($scope, Motion, Category, User, motion) {
|
||||
Motion.bindOne(motion.id, $scope, 'motion');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
Motion.loadRelations(motion);
|
||||
})
|
||||
|
||||
.controller('MotionCreateCtrl',
|
||||
function($scope, $state, $http, Motion, Agenda, User, Category, Workflow, Tag, Mediafile) {
|
||||
Agenda.bindAll({}, $scope, 'items');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||
|
||||
$scope.motion = {};
|
||||
$scope.save = function (motion) {
|
||||
Motion.create(motion).then(
|
||||
function(success) {
|
||||
$state.go('motions.motion.list');
|
||||
}
|
||||
);
|
||||
};
|
||||
})
|
||||
|
||||
.controller('MotionUpdateCtrl', [
|
||||
.controller('MotionListCtrl', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'Motion',
|
||||
'Category',
|
||||
'Tag',
|
||||
'Workflow',
|
||||
'User',
|
||||
function($scope, $state, Motion, Category, Tag, Workflow, User) {
|
||||
Motion.bindAll({}, $scope, 'motions');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
$scope.alert = {};
|
||||
|
||||
// setup table sorting
|
||||
$scope.sortColumn = 'identifier';
|
||||
$scope.filterPresent = '';
|
||||
$scope.reverse = false;
|
||||
// function to sort by clicked column
|
||||
$scope.toggleSort = function ( column ) {
|
||||
if ( $scope.sortColumn === column ) {
|
||||
$scope.reverse = !$scope.reverse;
|
||||
}
|
||||
$scope.sortColumn = column;
|
||||
};
|
||||
|
||||
// collect all states of all workflows
|
||||
// TODO: regard workflows only which are used by motions
|
||||
$scope.states = [];
|
||||
var workflows = Workflow.getAll();
|
||||
angular.forEach(workflows, function (workflow) {
|
||||
if (workflows.length > 1) {
|
||||
var wf = {}
|
||||
wf.name = "# "+workflow.name;
|
||||
$scope.states.push(wf);
|
||||
}
|
||||
angular.forEach(workflow.states, function (state) {
|
||||
$scope.states.push(state);
|
||||
});
|
||||
});
|
||||
|
||||
// hover edit actions
|
||||
$scope.hoverIn = function () {
|
||||
$scope.showEditActions = true;
|
||||
};
|
||||
$scope.hoverOut = function () {
|
||||
$scope.showEditActions = false;
|
||||
};
|
||||
|
||||
// save changed motion
|
||||
$scope.update = function (motion) {
|
||||
// get (unchanged) values from latest version for update method
|
||||
motion.title = motion.getTitle(-1);
|
||||
motion.text = motion.getText(-1);
|
||||
motion.reason = motion.getReason(-1);
|
||||
Motion.save(motion).then(
|
||||
function(success) {
|
||||
motion.quickEdit = false;
|
||||
$scope.alert.show = false;
|
||||
},
|
||||
function(error){
|
||||
var message = '';
|
||||
for (var e in error.data) {
|
||||
message += e + ': ' + error.data[e] + ' ';
|
||||
}
|
||||
$scope.alert = { type: 'danger', msg: message, show: true };
|
||||
});
|
||||
};
|
||||
|
||||
// *** delete mode functions ***
|
||||
$scope.isDeleteMode = false;
|
||||
// check all checkboxes
|
||||
$scope.checkAll = function () {
|
||||
angular.forEach($scope.motions, function (motion) {
|
||||
motion.selected = $scope.selectedAll;
|
||||
});
|
||||
};
|
||||
// uncheck all checkboxes if isDeleteMode is closed
|
||||
$scope.uncheckAll = function () {
|
||||
if (!$scope.isDeleteMode) {
|
||||
$scope.selectedAll = false;
|
||||
angular.forEach($scope.motions, function (motion) {
|
||||
motion.selected = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
// delete selected motions
|
||||
$scope.delete = function () {
|
||||
angular.forEach($scope.motions, function (motion) {
|
||||
if (motion.selected)
|
||||
Motion.destroy(motion.id);
|
||||
});
|
||||
$scope.isDeleteMode = false;
|
||||
$scope.uncheckAll();
|
||||
};
|
||||
// delete single motion
|
||||
$scope.deleteSingleMotion = function (motion) {
|
||||
Motion.destroy(motion.id);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('MotionDetailCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'Motion',
|
||||
'Agenda',
|
||||
'User',
|
||||
'Category',
|
||||
'Workflow',
|
||||
'Tag',
|
||||
'Mediafile',
|
||||
'Tag',
|
||||
'User',
|
||||
'Workflow',
|
||||
'motion',
|
||||
function ($scope, $state, $http, Motion, Agenda, User, Category, Workflow, Tag, Mediafile, motion) {
|
||||
Agenda.bindAll({}, $scope, 'items');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
function($scope, $http, Motion, Category, Mediafile, Tag, User, Workflow, motion) {
|
||||
Motion.bindOne(motion.id, $scope, 'motion');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
Motion.loadRelations(motion, 'agenda_item');
|
||||
// TODO: make 'motion.attachments' useable and itteratable in template
|
||||
// Motion.loadRelations(motion, 'attachments');
|
||||
|
||||
$scope.motion = motion;
|
||||
// get latest version for edit
|
||||
$scope.motion.title = $scope.motion.getTitle(-1);
|
||||
$scope.motion.text = $scope.motion.getText(-1);
|
||||
$scope.motion.reason = $scope.motion.getReason(-1);
|
||||
$scope.alert = {};
|
||||
$scope.isCollapsed = true;
|
||||
|
||||
$scope.support = function () {
|
||||
$http.post('/rest/motions/motion/' + motion.id + '/support/');
|
||||
}
|
||||
$scope.unsupport = function () {
|
||||
$http.delete('/rest/motions/motion/' + motion.id + '/support/');
|
||||
console.log(motion);
|
||||
}
|
||||
$scope.update_state = function (state) {
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': state.id});
|
||||
}
|
||||
$scope.reset_state = function (state_id) {
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {});
|
||||
}
|
||||
$scope.create_poll = function () {
|
||||
$http.post('/rest/motions/motion/' + motion.id + '/create_poll/', {})
|
||||
.success(function(data){
|
||||
$scope.alert.show = false;
|
||||
})
|
||||
.error(function(data){
|
||||
$scope.alert = { type: 'danger', msg: data.detail, show: true };
|
||||
});
|
||||
}
|
||||
$scope.delete_poll = function (poll) {
|
||||
poll.DSDestroy();
|
||||
}
|
||||
$scope.update_poll = function (poll) {
|
||||
poll.DSUpdate({
|
||||
motion_id: motion.id,
|
||||
votes: {"Yes": poll.yes, "No": poll.no, "Abstain": poll.abstain},
|
||||
votesvalid: poll.votesvalid,
|
||||
votesinvalid: poll.votesinvalid,
|
||||
votescast: poll.votescast
|
||||
})
|
||||
.then(function(success) {
|
||||
$scope.alert.show = false;
|
||||
poll.isEditMode = false;
|
||||
})
|
||||
.catch(function(error) {
|
||||
var message = '';
|
||||
for (var e in error.data) {
|
||||
message += e + ': ' + error.data[e] + ' ';
|
||||
}
|
||||
$scope.alert = { type: 'danger', msg: message, show: true };
|
||||
});
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
.controller('MotionCreateCtrl', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'gettext',
|
||||
'Motion',
|
||||
'MotionFormFieldFactory',
|
||||
'Category',
|
||||
'Config',
|
||||
'Mediafile',
|
||||
'Tag',
|
||||
'User',
|
||||
'Workflow',
|
||||
function($scope, $state, gettext, Motion, MotionFormFieldFactory, Category, Config, Mediafile, Tag, User, Workflow) {
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
|
||||
// get all form fields
|
||||
$scope.formFields = MotionFormFieldFactory.getFormFields();
|
||||
// override default values for create form
|
||||
for (var i = 0; i < $scope.formFields.length; i++) {
|
||||
if ($scope.formFields[i].key == "identifier") {
|
||||
$scope.formFields[i].hide = true;
|
||||
}
|
||||
if ($scope.formFields[i].key == "workflow_id") {
|
||||
// preselect default workflow
|
||||
$scope.formFields[i].defaultValue = Config.get('motions_workflow').value;
|
||||
}
|
||||
}
|
||||
$scope.save = function (motion) {
|
||||
Motion.save(motion).then(
|
||||
Motion.create(motion).then(
|
||||
function(success) {
|
||||
$state.go('motions.motion.list');
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
])
|
||||
|
||||
.controller('MotionUpdateCtrl', [
|
||||
'$scope',
|
||||
'$state',
|
||||
'gettext',
|
||||
'Motion',
|
||||
'Category',
|
||||
'Config',
|
||||
'Mediafile',
|
||||
'MotionFormFieldFactory',
|
||||
'Tag',
|
||||
'User',
|
||||
'Workflow',
|
||||
'motion',
|
||||
function($scope, $state, gettext, Motion, Category, Config, Mediafile, MotionFormFieldFactory, Tag, User, Workflow, motion) {
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
|
||||
// set initial values for form model
|
||||
$scope.model = motion;
|
||||
$scope.model.more = false;
|
||||
// get all form fields
|
||||
$scope.formFields = MotionFormFieldFactory.getFormFields();
|
||||
// override default values for update form
|
||||
for (var i = 0; i < $scope.formFields.length; i++) {
|
||||
if ($scope.formFields[i].key == "identifier") {
|
||||
// show identifier field
|
||||
$scope.formFields[i].hide = false;
|
||||
}
|
||||
if ($scope.formFields[i].key == "title") {
|
||||
// get title of latest version
|
||||
$scope.formFields[i].defaultValue = motion.getTitle(-1);
|
||||
}
|
||||
if ($scope.formFields[i].key == "text") {
|
||||
// get text of latest version
|
||||
$scope.formFields[i].defaultValue = motion.getText(-1);
|
||||
}
|
||||
if ($scope.formFields[i].key == "reason") {
|
||||
// get reason of latest version
|
||||
$scope.formFields[i].defaultValue = motion.getReason(-1);
|
||||
}
|
||||
if ($scope.formFields[i].key == "workflow_id") {
|
||||
// get saved workflow id from state
|
||||
$scope.formFields[i].defaultValue = motion.state.workflow_id;
|
||||
}
|
||||
}
|
||||
|
||||
// save form
|
||||
$scope.save = function (model) {
|
||||
Motion.save(model)
|
||||
.then(function(success) {
|
||||
$state.go('motions.motion.detail', {id: motion.id});
|
||||
})
|
||||
.catch(function(fallback) {
|
||||
console.log(fallback);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
@ -5,9 +5,8 @@
|
||||
<span ng-if="motion.versions.length > 1" >| Version {{ motion.active_version }}</span>
|
||||
</small>
|
||||
</h1>
|
||||
{{ motion.tags }}
|
||||
|
||||
{{ motion.agenda_item }}
|
||||
<!-- TODO: show list of speakers controls for { motion.agenda_item } -->
|
||||
|
||||
<div id="submenu">
|
||||
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default">
|
||||
@ -26,7 +25,7 @@
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</a>
|
||||
<!-- edit -->
|
||||
<a ui-sref="motions.motion.detail.update({id: motion.id })" os-perms="motions.can_manage"
|
||||
<a ng-if="motion.allowed_actions.update" ui-sref="motions.motion.detail.update({id: motion.id })"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Edit' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
@ -36,24 +35,212 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3 translate>Text</h3>
|
||||
<div class="white-space-pre-line">{{ motion.getText() }}</div>
|
||||
<div class="white-space-pre-line" ng-bind-html="motion.getText()"></div>
|
||||
|
||||
<h3 translate>Reason</h3>
|
||||
<div class="white-space-pre-line">{{ motion.getReason() }}</div>
|
||||
<!-- reason -->
|
||||
<div ng-if="motion.getReason() != ''">
|
||||
<h3 translate>Reason</h3>
|
||||
<div class="white-space-pre-line" ng-bind-html="motion.getReason()"></div>
|
||||
</div>
|
||||
|
||||
<!-- attachments
|
||||
TODO: make 'motion.attachments' useable and itteratable in template
|
||||
<div ng-if="motion.attachments">
|
||||
<h3 translate>Attachments</h3>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<!-- log -->
|
||||
<button type="button" class="btn btn-default spacer" ng-click="isCollapsed = !isCollapsed" translate>
|
||||
Show log
|
||||
</button>
|
||||
<div uib-collapse="isCollapsed">
|
||||
<div class="well well-sm">
|
||||
<ul class="list-unstyled">
|
||||
<li ng-repeat="message in motion.log_messages">
|
||||
<small>{{ message.message }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4">
|
||||
<div class="well">
|
||||
<!-- submitters -->
|
||||
<h3 translate>Submitters</h3>
|
||||
<div ng-repeat="submitter in motion.submitters">
|
||||
{{ submitter.get_full_name() }}<br>
|
||||
</div>
|
||||
|
||||
<!-- supporters -->
|
||||
<div ng-if="config('motions_min_supporters') > 0">
|
||||
<h3 translate>Supporters</h3>
|
||||
<ol>
|
||||
<li ng-repeat="supporters in motion.supporters">
|
||||
{{ supporters.get_full_name() }}
|
||||
</ol>
|
||||
<!-- support button -->
|
||||
<button ng-if="motion.allowed_actions.support" ng-click="support()" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-heart"></i>
|
||||
<translate>Support motion</translate>
|
||||
</button>
|
||||
<!-- unsupport button -->
|
||||
<button ng-if="motion.allowed_actions.unsupport" ng-click="unsupport()" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-heart-o"></i>
|
||||
<translate>Unsupport motion</translate>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 translate>State</h3>
|
||||
<span class="label" ng-class="'label-'+motion.state.css_class">
|
||||
{{ motion.state.name | translate }}
|
||||
</span>
|
||||
<div ng-if="motion.allowed_actions.change_state" class="spacer">
|
||||
<div class="btn-group-vertical spacer" role="group">
|
||||
<button ng-repeat="state in motion.state.getNextStates()" ng-click="update_state(state)"
|
||||
class="btn btn-default btn-sm">
|
||||
{{state.action_word}}
|
||||
</button>
|
||||
<button ng-if="motion.allowed_actions.reset_state" ng-click="reset_state()"
|
||||
class="btn btn-danger btn-xs">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<translate>Reset state</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 translate>Voting result</h3>
|
||||
<ol class="slimlist">
|
||||
<li ng-repeat="poll in motion.polls" class="spacer">
|
||||
<translate>Vote</translate>
|
||||
<button os-perms-lite="motions.can_manage" ng-click="poll.isEditMode=true;"
|
||||
class="btn btn-default btn-xs">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button os-perms="motions.can_manage" ng-click="delete_poll(poll)"
|
||||
class="btn btn-default btn-xs">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<div ng-show="poll.isEditMode" class="spacer">
|
||||
<form>
|
||||
<p>
|
||||
<translate>Special values</translate>:<br>
|
||||
<span class="badge badge-success">-1</span> = <translate>majority</translate><br>
|
||||
<span class="badge">-2</span> = <translate>undocumented</translate>
|
||||
</p>
|
||||
<alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
|
||||
{{alert.msg}}
|
||||
</alert>
|
||||
<!-- yes -->
|
||||
<div class="input-group col-sm-8 spacer">
|
||||
<div class="input-group-addon" title="{{ 'Yes' | translate }}"><i class="fa fa-thumbs-up"></i></div>
|
||||
<input type="number" ng-model="poll.yes" class="form-control input-sm" placeholder="{{ 'Yes' | translate }}">
|
||||
</div>
|
||||
<!-- no -->
|
||||
<div class="input-group col-sm-8">
|
||||
<div class="input-group-addon" title="{{ 'No' | translate }}"><i class="fa fa-thumbs-down"></i></div>
|
||||
<input type="number" ng-model="poll.no" class="form-control input-sm" placeholder="{{ 'No' | translate }}">
|
||||
</div>
|
||||
<!-- abstain -->
|
||||
<div class="input-group col-sm-8">
|
||||
<div class="input-group-addon" title="{{ 'Abstain' | translate }}"><b>∅</b></div>
|
||||
<input type="number" ng-model="poll.abstain" class="form-control input-sm" placeholder="{{ 'Abstain' | translate }}">
|
||||
</div>
|
||||
<!-- valid votes -->
|
||||
<div class="input-group col-sm-8 spacer">
|
||||
<div class="input-group-addon" title="{{ 'Valid votes' | translate }}"><i class="fa fa-check"></i></div>
|
||||
<input type="number" ng-model="poll.votesvalid" class="form-control input-sm" placeholder="{{ 'Valid votes' | translate }}">
|
||||
</div>
|
||||
<!-- invalid votes -->
|
||||
<div class="input-group col-sm-8">
|
||||
<div class="input-group-addon" title="{{ 'Invalid votes' | translate }}"><i class="fa fa-ban"></i></div>
|
||||
<input type="number" ng-model="poll.votesinvalid" class="form-control input-sm" placeholder="{{ 'Invalid votes' | translate }}">
|
||||
</div>
|
||||
<!-- votes cast -->
|
||||
<div class="input-group col-sm-8 spacer">
|
||||
<div class="input-group-addon" title="{{ 'Votes cast' | translate }}"><b>∑</b></div>
|
||||
<input type="number" ng-model="poll.votescast" class="form-control input-sm" placeholder="{{ 'Votes cast' | translate }}">
|
||||
</div>
|
||||
<!-- buttons -->
|
||||
<div class="spacer">
|
||||
<button type="submit" ng-click="update_poll(poll)" class="btn btn-primary" translate>
|
||||
Save
|
||||
</button>
|
||||
<button ng-click="poll.isEditMode=false;" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div ng-show="!poll.isEditMode && poll.yes >= -2">
|
||||
<!-- yes -->
|
||||
<div class="result_label">
|
||||
<i class="fa fa-thumbs-up"></i>
|
||||
<translate>Yes</translate>:
|
||||
</div>
|
||||
<div ng-if="poll.getYesPercent(true)">
|
||||
<uib-progressbar value="poll.getYesPercent(true)" type="success"></uib-progressbar>
|
||||
</div>
|
||||
<div class="result_value">{{ poll.yes }} {{ poll.getYesPercent() }}</div>
|
||||
<!-- no -->
|
||||
<div class="result_label">
|
||||
<i class="fa fa-thumbs-down"></i>
|
||||
<translate>No</translate>:
|
||||
</div>
|
||||
<div ng-if="poll.getNoPercent(true)">
|
||||
<uib-progressbar value="poll.getNoPercent(true)" type="danger"></uib-progressbar>
|
||||
</div>
|
||||
<div class="result_value">{{ poll.no }} {{ poll.getNoPercent() }}</div>
|
||||
<!-- abstain -->
|
||||
<div class="result_label">
|
||||
<b>∅</b>
|
||||
<translate>Abstain</translate>:
|
||||
</div>
|
||||
<div ng-if="poll.getAbstainPercent(true)">
|
||||
<uib-progressbar value="poll.getAbstainPercent(true)" type="warning"></uib-progressbar>
|
||||
</div>
|
||||
<div class="result_value">{{ poll.abstain }} {{ poll.getAbstainPercent() }}</div>
|
||||
<hr class="smallhr" ng-if="poll.votesvalid || poll.votesinvalid">
|
||||
<!-- valid votes -->
|
||||
<div ng-if="poll.votesvalid">
|
||||
<div class="result_label">
|
||||
<i class="fa fa-check"></i>
|
||||
<translate>Valid votes</translate>:
|
||||
</div>
|
||||
<div class="result_value">{{ poll.votesvalid }} {{ poll.getVotesValidPercent() }}</div>
|
||||
</div>
|
||||
<!-- invalid votes -->
|
||||
<div ng-if="poll.votesinvalid">
|
||||
<div class="result_label">
|
||||
<i class="fa fa-ban"></i>
|
||||
<translate>Invalid votes</translate>:
|
||||
</div>
|
||||
<div class="result_value">{{ poll.votesinvalid }} {{ poll.getVotesInvalidPercent() }}</div>
|
||||
</div>
|
||||
<hr class="smallhr" ng-if="poll.votescast">
|
||||
<!-- votes cast -->
|
||||
<div ng-if="poll.votescast">
|
||||
<div class="result_label">
|
||||
<b>∑</b>
|
||||
<translate>Votes cast</translate>:
|
||||
</div>
|
||||
<div class="result_value">{{ poll.votescast }} {{ poll.getVotesCastPercent() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ol>
|
||||
<button ng-if="motion.allowed_actions.create_poll" ng-click="create_poll()" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-bar-chart fa-lg"></i>
|
||||
<translate>New poll</translate>
|
||||
</button>
|
||||
|
||||
<h3 translate>Category</h3>
|
||||
{{ motion.category.name }}</a>
|
||||
|
||||
<h3 translate>Voting result</h3>
|
||||
-
|
||||
<h3 translate>Tags</h3>
|
||||
<p ng-repeat="tag in motion.tags">
|
||||
<span class="label label-default spacer-top">
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,88 +8,14 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form name="motionForm">
|
||||
<div ng-if="motion.id" class="form-group">
|
||||
<label for="inputIdentifier" translate>Identifier</label>
|
||||
<input type="text" ng-model="motion.identifier" class="form-control" name="inputIdentifier">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="selectItem" translate>Agenda item</label>
|
||||
<select ng-options="item.id as item.get_title for item in items"
|
||||
ng-model="motion.item" class="form-control" name="selectItem">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="selectSubmitter" translate>Submitter</label>
|
||||
<select multiple size="3" ng-options="user.id as user.get_short_name() for user in users"
|
||||
ng-model="motion.submitters_id" class="form-control" name="selectSubmitter" required>
|
||||
</select>
|
||||
<!-- TODO: use modern ui-select component with more information per user
|
||||
<ui-select multipe ng-model="motion.submitter" theme="bootstrap" name="selectSubmitter">
|
||||
<ui-select-match placeholder="{{ 'Select or search a participant...' | translate }}">
|
||||
{{ $select.selected.get_short_name() }}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="user.id as user in users | filter: $select.search">
|
||||
<div ng-bind-html="user.get_short_name() | highlight: $select.search"></div>
|
||||
<small ng-bind-html="user.structure_level | highlight: $select.search"></small>
|
||||
</ui-select-choices>
|
||||
</ui-select>-->
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputName" translate>Title</label>
|
||||
<input type="text" ng-model="motion.title" class="form-control" name="inputTitle" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="textText" translate>Text</label>
|
||||
<textarea ng-model="motion.text" class="form-control" name="textText" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="textReason" translate>Reason</label>
|
||||
<textarea ng-model="motion.reason" class="form-control" name="textReason" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="selectCategory" translate>Category</label>
|
||||
<select ng-options="category.id as category.name for category in categories"
|
||||
ng-model="motion.category_id" class="form-control" name="selectCategory">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="selectTags" translate>Tags</label>
|
||||
<select multiple ng-options="tag.id as tag.name for tag in tags"
|
||||
ng-model="motion.tags_id" class="form-control" name="selectTags">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="selectAttachments" translate>Attachments</label>
|
||||
<select ng-options="file.id as file.title for file in mediafiles"
|
||||
ng-model="motion.attachments_id" class="form-control" name="selectAttachments">
|
||||
</select>
|
||||
</div>
|
||||
<!-- TODO: show only if supporters is enabled -->
|
||||
<div class="form-group">
|
||||
<label for="selectSupporter" translate>Supporters</label>
|
||||
<ui-select multiple ng-model="motion.supporters_id" theme="bootstrap" name="selectSupporter">
|
||||
<ui-select-match placeholder="{{ 'Select or search a participant...' | translate }}">
|
||||
{{ $select.selected.get_short_name() }}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="user.id as user in users | filter: $select.search">
|
||||
<div ng-bind-html="user.get_short_name() | highlight: $select.search"></div>
|
||||
<small ng-bind-html="user.structure_level | highlight: $select.search"></small>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
<!-- TODO: preselect default workflow -->
|
||||
<div class="form-group">
|
||||
<label for="selectWorkflow" translate>Workflow</label>
|
||||
<select ng-options="workflow.id as workflow.name for workflow in workflows"
|
||||
ng-model="motion.workflow" class="form-control" name="selectWorkflow">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" ng-click="save(motion)" class="btn btn-primary" translate>
|
||||
Save
|
||||
</button>
|
||||
<button ui-sref="motions.motion.list" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
<form name="motionForm" ng-submit="save(model)">
|
||||
<formly-form model="model" fields="formFields">
|
||||
<button type="submit" ng-disabled="motionForm.$invalid" class="btn btn-primary" translate>
|
||||
Submit
|
||||
</button>
|
||||
<button ui-sref="motions.motion.list" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
</formly-form>
|
||||
</form>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<h1 translate>Motions</h1>
|
||||
|
||||
<div id="submenu">
|
||||
<a ui-sref="motions.motion.create" os-perms="motions.can_manage" class="btn btn-primary btn-sm">
|
||||
<a ui-sref="motions.motion.create" os-perms="motions.can_create" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-plus fa-lg"></i>
|
||||
<translate>New</translate>
|
||||
</a>
|
||||
@ -21,78 +21,182 @@
|
||||
|
||||
<div class="row form-group">
|
||||
<div class="col-sm-8">
|
||||
<!-- TODO: add filters -->
|
||||
<form class="form-inline">
|
||||
<!-- delete mode -->
|
||||
<div os-perms-lite="motions.can_manage" class="form-group">
|
||||
<label for="deleteSwitcher" translate>Delete mode</label>
|
||||
<switch id="deleteSwitcher" ng-model="isDeleteMode" ng-change="uncheckAll()"
|
||||
on="{{'On'|translate}}" off="{{'Off'|translate}}"
|
||||
class="green wide form-control">
|
||||
</switch>
|
||||
</div>
|
||||
<!-- delete button -->
|
||||
<a ng-show="isDeleteMode && (motions|filter:{selected:true}).length > 0"
|
||||
os-perms="motions.can_manage" ng-click="delete()"
|
||||
class="btn btn-primary btn-sm form-control">
|
||||
<i class="fa fa-trash fa-lg"></i>
|
||||
<translate>Delete selected motions</translate>
|
||||
</a>
|
||||
<!-- state filter -->
|
||||
|
||||
<select ng-model="stateFilter" class="form-control" id="stateFilter">
|
||||
<option value="" translate>--- Select state ---</option>
|
||||
<option ng-repeat="state in states" value="{{ state.id }}">{{ state.name }}</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" os-focus-me ng-model="filter.search" class="form-control"
|
||||
placeholder="{{ 'Filter' | translate}}">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon"><i class="fa fa-filter"></i></div>
|
||||
<input type="text" os-focus-me ng-model="filter.search" class="form-control"
|
||||
placeholder="{{ 'Filter' | translate}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<!-- TODO: Add motion.agenda_item to rest api -->
|
||||
<th ng-click="toggleSort('agenda_item')" class="sortable minimum">
|
||||
<translate>Agenda item</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'agenda_item' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<!-- projector column -->
|
||||
<th ng-show="!isDeleteMode" os-perms="core.can_manage_projector" class="firstColumn">
|
||||
<!-- delete selection column -->
|
||||
<th ng-show="isDeleteMode" os-perms-lite="motions.can_manage" class="firstColumn deleteColumn">
|
||||
<input type="checkbox" ng-model="selectedAll" ng-change="checkAll()">
|
||||
<th ng-click="toggleSort('identifier')" class="sortable minimum">
|
||||
<translate>Identifier</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'identifier' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<th ng-click="toggleSort('title')" class="sortable">
|
||||
<th ng-click="toggleSort('getTitle()')" class="sortable">
|
||||
<translate>Title</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'title' && header.sortable != false"
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'getTitle()' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<th ng-click="toggleSort('submitters')" class="sortable">
|
||||
<th ng-click="toggleSort('submitters')" class="sortable optional">
|
||||
<translate>Submitters</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'submitters' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<th ng-click="toggleSort('category')" class="sortable">
|
||||
<th ng-click="toggleSort('category')" class="sortable optional">
|
||||
<translate>Category</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'category' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<th os-perms="motions.can_manage core.can_manage_projector" class="minimum">
|
||||
<translate>Actions</translate>
|
||||
<th ng-click="toggleSort('state.name')" class="sortable optional">
|
||||
<translate>State</translate>
|
||||
<i class="pull-right fa" ng-show="sortColumn === 'state.name' && header.sortable != false"
|
||||
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
|
||||
</i>
|
||||
<tbody>
|
||||
<tr ng-repeat="motion in motions | filter: filter.search |
|
||||
orderBy: sortColumn:reverse" ng-class="{ 'activeline': motion.isProjected() }">
|
||||
<td> <!--TOOD: add agenda item reference -->
|
||||
<td><a ui-sref="motions.motion.detail({id: motion.id})">{{ motion.identifier }}</a>
|
||||
<td><a ui-sref="motions.motion.detail({id: motion.id})">
|
||||
{{ motion.getTitle() }}
|
||||
</a>
|
||||
<td class="optional">
|
||||
<div ng-repeat="submitter in motion.submitters">
|
||||
{{ submitter.get_full_name() }}<br>
|
||||
</div>
|
||||
<td class="optional">
|
||||
{{ motion.category.name }}
|
||||
<td os-perms="motions.can_manage core.can_manage_projector" class="nobr">
|
||||
<!-- project -->
|
||||
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
|
||||
<tr ng-repeat="motion in motions | filter: filter.search | filter: {state_id: stateFilter} |
|
||||
orderBy: sortColumn:reverse"
|
||||
class="animate-item"
|
||||
ng-class="{ 'activeline': motion.isProjected(), 'selected': motion.selected }">
|
||||
<!-- projector column -->
|
||||
<td ng-show="!isDeleteMode" os-perms-lite="core.can_manage_projector">
|
||||
<a class="btn btn-default btn-sm"
|
||||
ng-class="{ 'btn-primary': motion.isProjected() }"
|
||||
ng-click="motion.project()"
|
||||
title="{{ 'Project motion' | translate }}">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</a>
|
||||
<!-- edit -->
|
||||
<a ui-sref="motions.motion.detail.update({id: motion.id })" os-perms="motions.can_manage"
|
||||
class="btn btn-default btn-sm"
|
||||
title="{{ 'Edit' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<!-- delete -->
|
||||
<a os-perms="motions.can_manage" class="btn btn-danger btn-sm"
|
||||
ng-bootbox-confirm="Are you sure you want to delete <b>{{ motion.getTitle() }}</b>?"
|
||||
ng-bootbox-confirm-action="delete(motion)"
|
||||
title="{{ 'Delete' | translate }}">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</a>
|
||||
<!-- delete selection column -->
|
||||
<td ng-show="isDeleteMode" os-perms="motions.can_manage" class="deleteColumn">
|
||||
<input type="checkbox" ng-model="motion.selected">
|
||||
<!-- motion data colums -->
|
||||
<td ng-if="!motion.quickEdit">{{ motion.identifier }}
|
||||
<td ng-if="!motion.quickEdit" ng-mouseover="motion.hover=true" ng-mouseleave="motion.hover=false">
|
||||
<strong><a ui-sref="motions.motion.detail({id: motion.id})">{{ motion.getTitle() }}</a></strong>
|
||||
<div class="hoverActions" ng-class="{'hiddenDiv': !motion.hover}">
|
||||
<span ng-if="motion.allowed_actions.update">
|
||||
<a ui-sref="motions.motion.detail.update({ id: motion.id })" translate>Edit</a> |
|
||||
</span>
|
||||
<span ng-if="motion.allowed_actions.update">
|
||||
<a href="" os-perms="motions.can_manage" ng-click="motion.quickEdit=true" translate>QuickEdit</a> |
|
||||
</span>
|
||||
<span ng-if="motion.allowed_actions.delete">
|
||||
<!-- TODO: translate confirm message -->
|
||||
<a href="" class="text-danger"
|
||||
ng-bootbox-confirm="Are you sure you want to delete <b>{{ motion.getTitle() }}</b>?"
|
||||
ng-bootbox-confirm-action="deleteSingleMotion(motion)" translate>Delete</a>
|
||||
</span>
|
||||
</div>
|
||||
<td ng-if="!motion.quickEdit" class="optional">
|
||||
<div ng-repeat="submitter in motion.submitters">
|
||||
{{ submitter.get_full_name() }}<br>
|
||||
</div>
|
||||
<td ng-if="!motion.quickEdit" class="optional">
|
||||
{{ motion.category.name }}
|
||||
<td ng-if="!motion.quickEdit" class="optional">
|
||||
<span class="label" ng-class="'label-'+motion.state.css_class">
|
||||
{{ motion.state.name | translate }}
|
||||
</span>
|
||||
<!-- quickEdit columns -->
|
||||
<td ng-if="motion.quickEdit && motion.allowed_actions.update" colspan="5">
|
||||
<h4>{{ motion.getTitle() }} <span class="text-muted">– Quick Edit</span></h4>
|
||||
<alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
|
||||
{{alert.msg}}
|
||||
</alert>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label for="inputIdentifier" translate>Identifier</label>
|
||||
<input type="text" ng-model="motion.identifier" class="form-control input-sm"
|
||||
name="inputIdentifier">
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label for="selectCategory" translate>Category</label>
|
||||
<select ng-options="category.id as category.name for category in categories"
|
||||
ng-model="motion.category_id" class="form-control" name="selectCategory">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label for="selectSubmitter" translate>Submitters</label>
|
||||
<ui-select multiple ng-model="motion.submitters_id" name="selectSubmitter">
|
||||
<ui-select-match placeholder="{{ 'Select or search a submitter...' | translate }}">
|
||||
{{ $item.get_full_name() }}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="user.id as user in users | filter: $select.search">
|
||||
<div ng-bind-html="user.get_full_name() | highlight: $select.search"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label for="selectTags" translate>Tags</label>
|
||||
<ui-select multiple ng-model="motion.tags_id">
|
||||
<ui-select-match placeholder="{{ 'Select or search a tag...' | translate }}">
|
||||
{{ $item.name }}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="tag.id as tag in tags | filter: $select.search">
|
||||
{{ tag.name }}
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div ng-if="config('motions_min_supporters') > 0">
|
||||
<label for="selectSubmitter" translate>Supporters</label>
|
||||
<ui-select multiple ng-model="motion.supporters_id">
|
||||
<ui-select-match placeholder="{{ 'Select or search a supporter...' | translate }}">
|
||||
{{ $item.get_full_name() }}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="user.id as user in users | filter: $select.search">
|
||||
<div ng-bind-html="user.get_full_name() | highlight: $select.search"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer">
|
||||
<button ng-click="motion.quickEdit=false" class="btn btn-default pull-left" translate>
|
||||
Cancel
|
||||
</button>
|
||||
<button ng-if="motion.allowed_actions.update" ng-click="update(motion)" class="btn btn-primary" translate>
|
||||
Update
|
||||
</button>
|
||||
<a ng-if="motion.allowed_actions.update" ui-sref="motions.motion.detail.update({id: motion.id })"
|
||||
class="pull-right" translate>Edit motion...</a>
|
||||
</div>
|
||||
</table>
|
||||
|
@ -66,6 +66,16 @@ class MotionViewSet(ModelViewSet):
|
||||
result = False
|
||||
return result
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""
|
||||
Customized view endpoint to retrieve a motion.
|
||||
|
||||
Adds the allowed actions for the motion.
|
||||
"""
|
||||
response = super().retrieve(request, *args, **kwargs)
|
||||
response.data['allowed_actions'] = self.get_object().get_allowed_actions(request.user)
|
||||
return response
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Customized view endpoint to create a new motion.
|
||||
|
@ -9,6 +9,14 @@ angular.module('OpenSlidesApp.users', [])
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
useClass: jsDataModel,
|
||||
computed: {
|
||||
full_name: function () {
|
||||
return this.get_full_name();
|
||||
},
|
||||
short_name: function () {
|
||||
return this.get_short_name();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getResourceName: function () {
|
||||
return name;
|
||||
|
@ -40,7 +40,7 @@
|
||||
<div class="col-sm-8">
|
||||
<form class="form-inline">
|
||||
<!-- delete mode -->
|
||||
<div os-perms-lite="agenda.can_manage" class="form-group">
|
||||
<div os-perms-lite="users.can_manage" class="form-group">
|
||||
<label for="deleteSwitcher" translate>Delete mode</label>
|
||||
<switch id="deleteSwitcher" ng-model="isDeleteMode" ng-change="uncheckAll()"
|
||||
on="{{'On'|translate}}" off="{{'Off'|translate}}"
|
||||
|
@ -108,15 +108,17 @@ class CreateMotion(TestCase):
|
||||
self.assertEqual(motion.tags.get().name, 'test_tag_iRee3kiecoos4rorohth')
|
||||
|
||||
def test_with_workflow(self):
|
||||
self.assertEqual(config['motions_workflow'], '1')
|
||||
"""
|
||||
Test to create a motion with a specific workflow.
|
||||
"""
|
||||
response = self.client.post(
|
||||
reverse('motion-list'),
|
||||
{'title': 'test_title_eemuR5hoo4ru2ahgh5EJ',
|
||||
'text': 'test_text_ohviePopahPhoili7yee',
|
||||
'workflow': '2'})
|
||||
'workflow_id': '2'})
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
motion = Motion.objects.get()
|
||||
self.assertEqual(motion.state.workflow.pk, 2)
|
||||
self.assertEqual(Motion.objects.get().state.workflow_id, 2)
|
||||
|
||||
|
||||
class UpdateMotion(TestCase):
|
||||
@ -141,12 +143,15 @@ class UpdateMotion(TestCase):
|
||||
self.assertEqual(motion.identifier, 'test_identifier_jieseghohj7OoSah1Ko9')
|
||||
|
||||
def test_patch_workflow(self):
|
||||
self.assertEqual(config['motions_workflow'], '1')
|
||||
"""
|
||||
Tests to only update the workflow of a motion.
|
||||
"""
|
||||
response = self.client.patch(
|
||||
reverse('motion-detail', args=[self.motion.pk]),
|
||||
{'workflow': '2'})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
{'workflow_id': '2'})
|
||||
|
||||
motion = Motion.objects.get()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(motion.title, 'test_title_aeng7ahChie3waiR8xoh')
|
||||
self.assertEqual(motion.workflow, 2)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user