Reworked all motions templates.

motion detail:
- added progres bar for motionpoll
- added support/unsupport function
- show log

motion list:
- added state filter
- added css animations for enter/leave

motion form:
- use angular-formly (instead of old ng-fab-forms with no angular 1.4.x support)

general:
- Workflow states use new field 'css_class' (instead of unused
  'icon'). Added migration file.
- added 'allowed_actions' to rest api for each motion (by Norman)
- updated all JavaScript dependencies (bower.json)
This commit is contained in:
Emanuel Schuetze 2015-11-03 21:38:53 +01:00
parent c379544e97
commit ed72a90306
15 changed files with 711 additions and 403 deletions

View File

@ -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.14.2",
"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.13",
"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"
}
}

View File

@ -49,92 +49,49 @@ body {
color: red;
font-weight: bold;
}
.spacer {
.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; }
}
@ -296,9 +253,9 @@ tr.selected td {
padding-left: 20px;
}
.smallhr {
margin-top: 2px;
margin-bottom: 2px;
border-color: #333333;
margin-top: 5px;
margin-bottom: 5px;
border-color: #cccccc;
}
.resultcolumn {
font-weight: bold;

View File

@ -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

View File

@ -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>

View 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>

View 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'),
),
]

View File

@ -736,8 +736,12 @@ class State(RESTModelMixin, models.Model):
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)
"""
@ -764,7 +768,6 @@ class State(RESTModelMixin, models.Model):
This behavior can be changed by the form and view, e. g. via the
MotionDisableVersioningMixin.
"""
# TODO: preferred_for = ChoiceField
leave_old_version_active = models.BooleanField(default=False)
"""If true, new versions are not automaticly set active."""

View File

@ -49,7 +49,7 @@ class StateSerializer(ModelSerializer):
'id',
'name',
'action_word',
'icon',
'css_class',
'required_permission_to_see',
'allow_support',
'allow_create_poll',
@ -77,9 +77,17 @@ 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):

View File

@ -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

View File

@ -40,7 +40,7 @@ angular.module('OpenSlidesApp.motions', [])
}
])
// Load all MotionWorkflows at stateup
// Load all MotionWorkflows at startup
.run([
'Workflow',
function (Workflow) {
@ -53,7 +53,7 @@ angular.module('OpenSlidesApp.motions', [])
'Config',
function (DS, Config) {
return DS.defineResource({
name: 'motions/poll',
name: 'motions/motionpoll',
relations: {
belongsTo: {
'motions/motion': {
@ -63,61 +63,91 @@ angular.module('OpenSlidesApp.motions', [])
}
},
methods: {
getYesPercent: function () {
getYesPercent: function (valueOnly) {
var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITHOUT_INVALID" && this.votesvalid > 0) {
return "(" + Math.round(this.yes * 100 / this.votesvalid * 10) / 10 + " %)";
} else if (config == "WITH_INVALID" && this.votescast > 0) {
return "(" + Math.round(this.yes * 100 / (this.votescast) * 10) / 10 + " %)";
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 {
return null;
returnvalue = null;
}
if (!valueOnly && returnvalue != null) {
returnvalue = "(" + returnvalue + "%)";
}
return returnvalue;
},
getNoPercent: function () {
getNoPercent: function (valueOnly) {
var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITHOUT_INVALID" && this.votesvalid > 0) {
return "(" + Math.round(this.no * 100 / this.votesvalid * 10) / 10 + " %)";
} else if (config == "WITH_INVALID" && this.votescast > 0) {
return "(" + Math.round(this.no * 100 / (this.votescast) * 10) / 10 + " %)";
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 {
return null;
returnvalue = null;
}
if (!valueOnly && returnvalue != null) {
returnvalue = "(" + returnvalue + "%)";
}
return returnvalue;
},
getAbstainPercent: function () {
getAbstainPercent: function (valueOnly) {
var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITHOUT_INVALID" && this.votesvalid > 0) {
return "(" + Math.round(this.abstain * 100 / this.votesvalid * 10) / 10 + " %)";
} else if (config == "WITH_INVALID" && this.votescast > 0) {
return "(" + Math.round(this.abstain * 100 / (this.votescast) * 10) / 10 + " %)";
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 {
return null;
returnvalue = null;
}
if (!valueOnly && returnvalue != null) {
returnvalue = "(" + returnvalue + "%)";
}
return returnvalue;
},
getVotesValidPercent: function () {
getVotesValidPercent: function (valueOnly) {
var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITHOUT_INVALID") {
return "(100 %)";
} else if (config == "WITH_INVALID") {
return "(" + Math.round(this.votesvalid * 100 / (this.votescast) * 10) / 10 + " %)";
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 {
return null;
returnvalue = null;
}
if (!valueOnly && returnvalue != null) {
returnvalue = "(" + returnvalue + "%)";
}
return returnvalue;
},
getVotesInvalidPercent: function () {
getVotesInvalidPercent: function (valueOnly) {
var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITH_INVALID") {
return "(" + Math.round(this.votesinvalid * 100 / (this.votescast) * 10) / 10 + " %)";
var returnvalue;
if (config == "WITH_INVALID" && this.voteinvalid >= 0) {
returnvalue = Math.round(this.votesinvalid * 100 / (this.votescast) * 10) / 10;
} else {
return null;
returnvalue = null;
}
if (!valueOnly && returnvalue != null) {
returnvalue = "(" + returnvalue + "%)";
}
return returnvalue;
},
getVotesCastPercent: function () {
getVotesCastPercent: function (valueOnly) {
var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITH_INVALID") {
return "(100 %)";
var returnvalue;
if (config == "WITH_INVALID" && this.votecast >= 0) {
returnvalue = 100;
} else {
return null;
returnvalue = null;
}
if (!valueOnly && returnvalue != null) {
returnvalue = "(" + returnvalue + "%)";
}
return returnvalue;
}
}
});
@ -183,6 +213,10 @@ angular.module('OpenSlidesApp.motions', [])
localField: 'tags',
localKeys: 'tags_id',
},
'mediafiles/mediafile': {
localField: 'attachments',
localKeys: 'attachments_id',
},
'users/user': [
{
localField: 'submitters',
@ -193,7 +227,7 @@ angular.module('OpenSlidesApp.motions', [])
localKeys: 'supporters_id',
}
],
'motions/poll': {
'motions/motionpoll': {
localField: 'polls',
foreignKey: 'motion_id',
}
@ -209,6 +243,144 @@ angular.module('OpenSlidesApp.motions', [])
}
])
// 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',
@ -252,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();
}
@ -259,9 +434,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
})
.state('motions.motion.create', {
resolve: {
items: function(Agenda) {
return Agenda.findAll();
},
categories: function(Category) {
return Category.findAll();
},
@ -273,6 +445,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
},
mediafiles: function(Mediafile) {
return Mediafile.findAll();
},
workflows: function(Workflow) {
return Workflow.findAll();
}
}
})
@ -287,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', {
@ -297,9 +475,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
'@motions.motion': {}
},
resolve: {
items: function(Agenda) {
return Agenda.findAll();
},
categories: function(Category) {
return Category.findAll();
},
@ -311,6 +486,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
},
mediafiles: function(Mediafile) {
return Mediafile.findAll();
},
workflows: function(Workflow) {
return Workflow.findAll();
}
}
})
@ -351,11 +529,16 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
'$state',
'Motion',
'Category',
'Tag',
'Workflow',
'User',
function($scope, $state, Motion, Category, 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';
@ -369,6 +552,21 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
$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;
@ -432,24 +630,37 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
.controller('MotionDetailCtrl', [
'$scope',
'$http',
'Motion',
'Category',
'Workflow',
'Mediafile',
'Tag',
'User',
'Workflow',
'motion',
'$http',
function($scope, Motion, Category, Workflow, User, motion, $http) {
function($scope, $http, Motion, Category, Mediafile, Tag, User, Workflow, motion) {
Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories');
Workflow.bindAll({}, $scope, 'workflows');
Mediafile.bindAll({}, $scope, 'mediafiles');
Tag.bindAll({}, $scope, 'tags');
User.bindAll({}, $scope, 'users');
Workflow.bindAll({}, $scope, 'workflows');
Motion.loadRelations(motion, 'agenda_item');
var state = motion.state;
state.getNextStates()
$scope.alert = {}; // TODO: show alert in template
// TODO: make 'motion.attachments' useable and itteratable in template
// Motion.loadRelations(motion, 'attachments');
$scope.update_state = function (state_id) {
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': state_id});
$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/', {});
@ -473,22 +684,53 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
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',
function($scope, $state, $http, Motion, Agenda, User, Category, Workflow, Tag, Mediafile) {
Agenda.bindAll({}, $scope, 'items');
User.bindAll({}, $scope, 'users');
.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');
Workflow.bindAll({}, $scope, 'workflows');
Tag.bindAll({}, $scope, 'tags');
Mediafile.bindAll({}, $scope, 'mediafiles');
Tag.bindAll({}, $scope, 'tags');
User.bindAll({}, $scope, 'users');
Workflow.bindAll({}, $scope, 'workflows');
$scope.motion = {};
// 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.create(motion).then(
function(success) {
@ -496,40 +738,68 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
}
);
};
})
}
])
.controller('MotionUpdateCtrl', [
'$scope',
'$state',
'$http',
'gettext',
'Motion',
'Agenda',
'User',
'Category',
'Workflow',
'Tag',
'Config',
'Mediafile',
'MotionFormFieldFactory',
'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, $state, gettext, Motion, Category, Config, Mediafile, MotionFormFieldFactory, Tag, User, Workflow, 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');
$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.save = function (motion) {
Motion.save(motion).then(
function(success) {
$state.go('motions.motion.list');
// 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);
});
};
}
])

View File

@ -6,11 +6,7 @@
</small>
</h1>
agenda_id: {{ motion.agenda_item }}
<br><br>state: {{ motion.state }}
<br><br>next states: {{ motion.state.getNextStates() }}
<!-- 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">
@ -29,7 +25,7 @@ agenda_id: {{ motion.agenda_item }}
<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>
@ -41,42 +37,73 @@ agenda_id: {{ motion.agenda_item }}
<h3 translate>Text</h3>
<div class="white-space-pre-line" ng-bind-html="motion.getText()"></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 label-primary">{{ motion.state.name | translate }}</span>
<button os-perms-lite="motions.can_manage" ng-click="motion.isStatusEditMode=true"
class="btn btn-default btn-xs">
<i class="fa fa-pencil"></i>
</button>
<div ng-if="motion.isStatusEditMode" os-perms-lite="motions.can_manage">
<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_id in motion.state.next_states_id" class="btn btn-default btn-sm"
ng-click="update_state(state_id)">
State #{{state_id}}
<button ng-repeat="state in motion.state.getNextStates()" ng-click="update_state(state)"
class="btn btn-default btn-sm">
{{state.action_word}}
</button>
</div>
<div>
<button ng-click="reset_state()"
class="btn btn-danger btn-xs spacer">
<i class="fa fa-exclamation-triangle"></i> Reset
<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>
@ -93,8 +120,16 @@ agenda_id: {{ motion.agenda_item }}
class="btn btn-default btn-xs">
<i class="fa fa-times"></i>
</button>
<br>
<form ng-show="poll.isEditMode">
<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>
@ -135,25 +170,35 @@ agenda_id: {{ motion.agenda_item }}
</button>
</div>
</form>
<div ng-show="!poll.isEditMode && poll.yes">
</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 class="result_value">{{ poll.yes }} {{poll.getYesPercent()}}</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 class="result_value">{{ poll.no }} {{poll.getNoPercent()}}</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="resutl_label">
<div class="result_label">
<b>&empty;</b>
<translate>Abstain</translate>:
</div>
<div class="result_value">{{ poll.abstain }} {{poll.getAbstainPercent()}}</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">
@ -161,7 +206,7 @@ agenda_id: {{ motion.agenda_item }}
<i class="fa fa-check"></i>
<translate>Valid votes</translate>:
</div>
<div class="result_value">{{ poll.votesvalid }} {{poll.getVotesValidPercent()}}</div>
<div class="result_value">{{ poll.votesvalid }} {{ poll.getVotesValidPercent() }}</div>
</div>
<!-- invalid votes -->
<div ng-if="poll.votesinvalid">
@ -169,20 +214,20 @@ agenda_id: {{ motion.agenda_item }}
<i class="fa fa-ban"></i>
<translate>Invalid votes</translate>:
</div>
<div class="result_value">{{ poll.votesinvalid }} {{poll.getVotesInvalidPercent()}}</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="resutl_label">
<div class="result_label">
<b>&sum;</b>
<translate>Votes cast</translate>:
</div>
<div class="result_value">{{ poll.votescast }} {{poll.getVotesCastPercent()}}</div>
<div class="result_value">{{ poll.votescast }} {{ poll.getVotesCastPercent() }}</div>
</div>
</div>
</ol>
<button ng-click="create_poll()" class="btn btn-default btn-sm">
<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>
@ -191,12 +236,11 @@ agenda_id: {{ motion.agenda_item }}
{{ motion.category.name }}</a>
<h3 translate>Tags</h3>
<span ng-repeat="tag in motion.tags">
<span class="label label-default">
<p ng-repeat="tag in motion.tags">
<span class="label label-default spacer-top">
{{ tag.name }}
</span>
&nbsp;
</span>
</p>
</div>
</div>
</div>

View File

@ -8,94 +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>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="form-group">
<label for="inputTitle" 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>
<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 class="form-group">
<label for="selectAttachments" translate>Attachments</label>
<ui-select multiple ng-model="motion.attachments_id">
<ui-select-match placeholder="{{ 'Select or search an attachment...' | translate }}">
{{ $item.name }}
</ui-select-match>
<ui-select-choices repeat="file.id as file in mediafiles | filter: $select.search">
{{ file.title }}
</ui-select-choices>
</ui-select>
</div>
<!-- show supporters if enabled -->
<div class="form-group" ng-if="config('motions_min_supporters') > 0">
<label for="selectSupporter" 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>
<!-- TODO: preselect default workflow
does not work: ng-init="motion.workflow = config('motions_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
<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>

View File

@ -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>
@ -37,13 +37,21 @@
<i class="fa fa-trash fa-lg"></i>
<translate>Delete selected motions</translate>
</a>
<!-- TODO: add filters -->
<!-- state filter -->
&nbsp;
<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">
<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">
@ -80,8 +88,9 @@
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
<tbody>
<tr ng-repeat="motion in motions | filter: filter.search |
<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">
@ -98,12 +107,19 @@
<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 os-perms-lite="motions.can_manage" class="hoverActions" ng-class="{'hiddenDiv': !motion.hover}">
<a ui-sref="motions.motion.detail.update({id: motion.id })" translate>Edit</a> |
<a href="" ng-click="motion.quickEdit=true" translate>QuickEdit</a> |
<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">
@ -112,9 +128,11 @@
<td ng-if="!motion.quickEdit" class="optional">
{{ motion.category.name }}
<td ng-if="!motion.quickEdit" class="optional">
<span class="label label-primary">{{ motion.state.name | translate }}</span>
<span class="label" ng-class="'label-'+motion.state.css_class">
{{ motion.state.name | translate }}
</span>
<!-- quickEdit columns -->
<td ng-if="motion.quickEdit" colspan="5">
<td ng-if="motion.quickEdit && motion.allowed_actions.update" colspan="5">
<h4>{{ motion.getTitle() }} <span class="text-muted">&ndash; Quick Edit</span></h4>
<alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
{{alert.msg}}
@ -170,20 +188,15 @@
</ui-select>
</div>
</div>
<div class="col-xs-6">
<label for="selectState" translate>State</label>
<select ng-model="motion.state" class="form-control" name="selectState">
</select>
</div>
</div>
<div class="spacer">
<button ng-click="motion.quickEdit=false" class="btn btn-default pull-left" translate>
Cancel
</button> &nbsp;
<button ng-click="update(motion)" class="btn btn-primary" translate>
<button ng-if="motion.allowed_actions.update" ng-click="update(motion)" class="btn btn-primary" translate>
Update
</button>
<a ui-sref="motions.motion.detail.update({id: motion.id })" class="pull-right"
translate>Edit motion...</a>
<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>

View File

@ -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.

View File

@ -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;