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", "name": "OpenSlides",
"private": true, "private": true,
"dependencies": { "dependencies": {
"lodash": "~3.0.1", "lodash": "~3.10.0",
"jquery": "~2.1.4", "jquery": "~2.1.4",
"jquery.cookie": "~1.4.1", "jquery.cookie": "~1.4.1",
"bootstrap-css-only": "~3.3.4", "bootstrap-css-only": "~3.3.5",
"angular": "~1.3.15", "angular": "~1.4.7",
"angular-bootstrap": "~0.14.2", "angular-messages": "~1.4.7",
"angular-messages": "~1.3.15", "angular-animate": "~1.4.7",
"angular-animate": "~1.3.15", "angular-sanitize": "~1.4.7",
"angular-csv-import": "~0.0.15", "angular-bootstrap": "~0.14.3",
"angular-loading-bar": "~0.7.1", "angular-csv-import": "~0.0.26",
"angular-ui-router": "~0.2.13", "angular-formly-templates-bootstrap": "~6.1.5",
"angular-ui-select": "~0.13", "angular-formly": "~7.3.2",
"angular-ui-switch": "~0.1.0", "angular-loading-bar": "~0.8.0",
"angular-ui-tree": "~2.2.0", "angular-ui-router": "~0.2.15",
"angular-gettext": "~2.0.2", "angular-ui-select": "~0.13.1",
"angular-sanitize": "~1.3.15", "angular-ui-switch": "~0.1.1",
"angular-ui-tree": "~2.10.0",
"angular-gettext": "~2.1.2",
"angular-xeditable": "~0.1.9", "angular-xeditable": "~0.1.9",
"ng-fab-form": "~1.2.7", "ngBootbox": "~0.1.2",
"ngBootbox": "~0.0.5",
"sockjs": "~0.3.4", "sockjs": "~0.3.4",
"font-awesome-bower": "4.3.0", "font-awesome-bower": "~4.4.0",
"js-data": "~2.3.0", "js-data": "~2.8.1",
"js-data-angular": "~3.0.0", "js-data-angular": "~3.1.0",
"ng-file-upload": "~9.1.2" "ng-file-upload": "~9.1.2"
} }
} }

View File

@ -49,92 +49,49 @@ body {
color: red; color: red;
font-weight: bold; font-weight: bold;
} }
.spacer { .spacer, .spacer-top {
margin-top: 7px; margin-top: 7px;
} }
.spacer-right {
margin-right: 5px;
}
.hoverActions { .hoverActions {
font-size: 85%; font-size: 85%;
} }
.hiddenDiv { .hiddenDiv {
visibility: hidden; 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 { /* ngAnimate classes */
opacity: 0; } .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 { @keyframes fade-out {
color: #fff; 0% { opacity: 1; background: none; }
margin: 0; 25% { opacity: 1; background: #f8efc0; }
position: relative; 100% { opacity: 0; background: none; }
font-size: 14px; }
overflow: visible; @keyframes fade-in {
background: #c00640; } 0% { opacity: 0; background: none; }
.validation ul { 25% { opacity: 1; background: #dff0d8; }
display: block; 100% { opacity: 1; background: none; }
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; }
@ -296,9 +253,9 @@ tr.selected td {
padding-left: 20px; padding-left: 20px;
} }
.smallhr { .smallhr {
margin-top: 2px; margin-top: 5px;
margin-bottom: 2px; margin-bottom: 5px;
border-color: #333333; border-color: #cccccc;
} }
.resultcolumn { .resultcolumn {
font-weight: bold; font-weight: bold;

View File

@ -6,8 +6,9 @@
angular.module('OpenSlidesApp.core.site', [ angular.module('OpenSlidesApp.core.site', [
'OpenSlidesApp.core', 'OpenSlidesApp.core',
'ui.router', 'ui.router',
'formly',
'formlyBootstrap',
'ngBootbox', 'ngBootbox',
'ngFabForm',
'ngMessages', 'ngMessages',
'ngCsvImport', 'ngCsvImport',
'ngSanitize', // TODO: only use this in functions that need it. 'ngSanitize', // TODO: only use this in functions that need it.
@ -260,13 +261,6 @@ angular.module('OpenSlidesApp.core.site', [
$locationProvider.html5Mode(true); $locationProvider.html5Mode(true);
}) })
// config for ng-fab-form
.config(function(ngFabFormProvider) {
ngFabFormProvider.extendConfig({
setAsteriskForRequiredLabel: true
});
})
// Helper to add ui.router states at runtime. // Helper to add ui.router states at runtime.
// Needed for the django url_patterns. // Needed for the django url_patterns.
.provider('runtimeStates', function($stateProvider) { .provider('runtimeStates', function($stateProvider) {
@ -299,6 +293,26 @@ angular.module('OpenSlidesApp.core.site', [
editableOptions.theme = 'bs3'; 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 // html-tag os-form-field to generate generic from fields
// TODO: make it possible to use other fields then config 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) next_states = models.ManyToManyField('self', symmetrical=False)
"""A many-to-many relation to all states, that can be choosen from this state.""" """A many-to-many relation to all states, that can be choosen from this state."""
icon = models.CharField(max_length=255) css_class = models.CharField(max_length=255, default='primary')
"""A string representing the url to the icon-image.""" """
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) 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 This behavior can be changed by the form and view, e. g. via the
MotionDisableVersioningMixin. MotionDisableVersioningMixin.
""" """
# TODO: preferred_for = ChoiceField
leave_old_version_active = models.BooleanField(default=False) leave_old_version_active = models.BooleanField(default=False)
"""If true, new versions are not automaticly set active.""" """If true, new versions are not automaticly set active."""

View File

@ -49,7 +49,7 @@ class StateSerializer(ModelSerializer):
'id', 'id',
'name', 'name',
'action_word', 'action_word',
'icon', 'css_class',
'required_permission_to_see', 'required_permission_to_see',
'allow_support', 'allow_support',
'allow_create_poll', 'allow_create_poll',
@ -77,9 +77,17 @@ class MotionLogSerializer(ModelSerializer):
""" """
Serializer for motion.models.MotionLog objects. Serializer for motion.models.MotionLog objects.
""" """
message = SerializerMethodField()
class Meta: class Meta:
model = MotionLog 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): class MotionPollSerializer(ModelSerializer):

View File

@ -189,13 +189,16 @@ def create_builtin_workflows(sender, **kwargs):
allow_submitter_edit=True) allow_submitter_edit=True)
state_1_2 = State.objects.create(name=ugettext_noop('accepted'), state_1_2 = State.objects.create(name=ugettext_noop('accepted'),
workflow=workflow_1, 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'), state_1_3 = State.objects.create(name=ugettext_noop('rejected'),
workflow=workflow_1, 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'), state_1_4 = State.objects.create(name=ugettext_noop('not decided'),
workflow=workflow_1, 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) state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
workflow_1.first_state = state_1_1 workflow_1.first_state = state_1_1
workflow_1.save() workflow_1.save()
@ -216,35 +219,43 @@ def create_builtin_workflows(sender, **kwargs):
state_2_3 = State.objects.create(name=ugettext_noop('accepted'), state_2_3 = State.objects.create(name=ugettext_noop('accepted'),
workflow=workflow_2, workflow=workflow_2,
action_word=ugettext_noop('Accept'), action_word=ugettext_noop('Accept'),
versioning=True) versioning=True,
css_class='success')
state_2_4 = State.objects.create(name=ugettext_noop('rejected'), state_2_4 = State.objects.create(name=ugettext_noop('rejected'),
workflow=workflow_2, workflow=workflow_2,
action_word=ugettext_noop('Reject'), action_word=ugettext_noop('Reject'),
versioning=True) versioning=True,
css_class='danger')
state_2_5 = State.objects.create(name=ugettext_noop('withdrawed'), state_2_5 = State.objects.create(name=ugettext_noop('withdrawed'),
workflow=workflow_2, workflow=workflow_2,
action_word=ugettext_noop('Withdraw'), action_word=ugettext_noop('Withdraw'),
versioning=True) versioning=True,
css_class='default')
state_2_6 = State.objects.create(name=ugettext_noop('adjourned'), state_2_6 = State.objects.create(name=ugettext_noop('adjourned'),
workflow=workflow_2, workflow=workflow_2,
action_word=ugettext_noop('Adjourn'), action_word=ugettext_noop('Adjourn'),
versioning=True) versioning=True,
css_class='default')
state_2_7 = State.objects.create(name=ugettext_noop('not concerned'), state_2_7 = State.objects.create(name=ugettext_noop('not concerned'),
workflow=workflow_2, workflow=workflow_2,
action_word=ugettext_noop('Do not concern'), 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'), state_2_8 = State.objects.create(name=ugettext_noop('commited a bill'),
workflow=workflow_2, workflow=workflow_2,
action_word=ugettext_noop('Commit a bill'), 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'), state_2_9 = State.objects.create(name=ugettext_noop('needs review'),
workflow=workflow_2, workflow=workflow_2,
action_word=ugettext_noop('Needs review'), 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)'), state_2_10 = State.objects.create(name=ugettext_noop('rejected (not authorized)'),
workflow=workflow_2, workflow=workflow_2,
action_word=ugettext_noop('Reject (not authorized)'), 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_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) 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 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([ .run([
'Workflow', 'Workflow',
function (Workflow) { function (Workflow) {
@ -53,7 +53,7 @@ angular.module('OpenSlidesApp.motions', [])
'Config', 'Config',
function (DS, Config) { function (DS, Config) {
return DS.defineResource({ return DS.defineResource({
name: 'motions/poll', name: 'motions/motionpoll',
relations: { relations: {
belongsTo: { belongsTo: {
'motions/motion': { 'motions/motion': {
@ -63,61 +63,91 @@ angular.module('OpenSlidesApp.motions', [])
} }
}, },
methods: { methods: {
getYesPercent: function () { getYesPercent: function (valueOnly) {
var config = Config.get('motions_poll_100_percent_base').value; var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITHOUT_INVALID" && this.votesvalid > 0) { var returnvalue;
return "(" + Math.round(this.yes * 100 / this.votesvalid * 10) / 10 + " %)"; if (config == "WITHOUT_INVALID" && this.votesvalid > 0 && this.yes >= 0) {
} else if (config == "WITH_INVALID" && this.votescast > 0) { returnvalue = Math.round(this.yes * 100 / this.votesvalid * 10) / 10;
return "(" + Math.round(this.yes * 100 / (this.votescast) * 10) / 10 + " %)"; } else if (config == "WITH_INVALID" && this.votescast > 0 && this.yes >= 0) {
returnvalue = Math.round(this.yes * 100 / (this.votescast) * 10) / 10;
} else { } 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; var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITHOUT_INVALID" && this.votesvalid > 0) { var returnvalue;
return "(" + Math.round(this.no * 100 / this.votesvalid * 10) / 10 + " %)"; if (config == "WITHOUT_INVALID" && this.votesvalid > 0 && this.no >= 0) {
} else if (config == "WITH_INVALID" && this.votescast > 0) { returnvalue = Math.round(this.no * 100 / this.votesvalid * 10) / 10;
return "(" + Math.round(this.no * 100 / (this.votescast) * 10) / 10 + " %)"; } else if (config == "WITH_INVALID" && this.votescast > 0 && this.no >= 0) {
returnvalue = Math.round(this.no * 100 / (this.votescast) * 10) / 10;
} else { } 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; var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITHOUT_INVALID" && this.votesvalid > 0) { var returnvalue;
return "(" + Math.round(this.abstain * 100 / this.votesvalid * 10) / 10 + " %)"; if (config == "WITHOUT_INVALID" && this.votesvalid > 0 && this.abstain >= 0) {
} else if (config == "WITH_INVALID" && this.votescast > 0) { returnvalue = Math.round(this.abstain * 100 / this.votesvalid * 10) / 10;
return "(" + Math.round(this.abstain * 100 / (this.votescast) * 10) / 10 + " %)"; } else if (config == "WITH_INVALID" && this.votescast > 0 && this.abstain >= 0) {
returnvalue = Math.round(this.abstain * 100 / (this.votescast) * 10) / 10;
} else { } 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; var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITHOUT_INVALID") { var returnvalue;
return "(100 %)"; if (config == "WITHOUT_INVALID" && this.votevalid >= 0) {
} else if (config == "WITH_INVALID") { returnvalue = 100;
return "(" + Math.round(this.votesvalid * 100 / (this.votescast) * 10) / 10 + " %)"; } else if (config == "WITH_INVALID" && this.votevalid >= 0) {
returnvalue = Math.round(this.votesvalid * 100 / (this.votescast) * 10) / 10;
} else { } 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; var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITH_INVALID") { var returnvalue;
return "(" + Math.round(this.votesinvalid * 100 / (this.votescast) * 10) / 10 + " %)"; if (config == "WITH_INVALID" && this.voteinvalid >= 0) {
returnvalue = Math.round(this.votesinvalid * 100 / (this.votescast) * 10) / 10;
} else { } 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; var config = Config.get('motions_poll_100_percent_base').value;
if (config == "WITH_INVALID") { var returnvalue;
return "(100 %)"; if (config == "WITH_INVALID" && this.votecast >= 0) {
returnvalue = 100;
} else { } else {
return null; returnvalue = null;
} }
if (!valueOnly && returnvalue != null) {
returnvalue = "(" + returnvalue + "%)";
}
return returnvalue;
} }
} }
}); });
@ -183,6 +213,10 @@ angular.module('OpenSlidesApp.motions', [])
localField: 'tags', localField: 'tags',
localKeys: 'tags_id', localKeys: 'tags_id',
}, },
'mediafiles/mediafile': {
localField: 'attachments',
localKeys: 'attachments_id',
},
'users/user': [ 'users/user': [
{ {
localField: 'submitters', localField: 'submitters',
@ -193,7 +227,7 @@ angular.module('OpenSlidesApp.motions', [])
localKeys: 'supporters_id', localKeys: 'supporters_id',
} }
], ],
'motions/poll': { 'motions/motionpoll': {
localField: 'polls', localField: 'polls',
foreignKey: 'motion_id', 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) { .factory('Category', ['DS', function(DS) {
return DS.defineResource({ return DS.defineResource({
name: 'motions/category', name: 'motions/category',
@ -252,6 +424,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
categories: function(Category) { categories: function(Category) {
return Category.findAll(); return Category.findAll();
}, },
tags: function(Tag) {
return Tag.findAll();
},
users: function(User) { users: function(User) {
return User.findAll(); return User.findAll();
} }
@ -259,9 +434,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
}) })
.state('motions.motion.create', { .state('motions.motion.create', {
resolve: { resolve: {
items: function(Agenda) {
return Agenda.findAll();
},
categories: function(Category) { categories: function(Category) {
return Category.findAll(); return Category.findAll();
}, },
@ -273,6 +445,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
}, },
mediafiles: function(Mediafile) { mediafiles: function(Mediafile) {
return Mediafile.findAll(); return Mediafile.findAll();
},
workflows: function(Workflow) {
return Workflow.findAll();
} }
} }
}) })
@ -287,9 +462,12 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
users: function(User) { users: function(User) {
return User.findAll(); return User.findAll();
}, },
mediafiles: function(Mediafile) {
return Mediafile.findAll();
},
tags: function(Tag) { tags: function(Tag) {
return Tag.findAll(); return Tag.findAll();
}, }
} }
}) })
.state('motions.motion.detail.update', { .state('motions.motion.detail.update', {
@ -297,9 +475,6 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
'@motions.motion': {} '@motions.motion': {}
}, },
resolve: { resolve: {
items: function(Agenda) {
return Agenda.findAll();
},
categories: function(Category) { categories: function(Category) {
return Category.findAll(); return Category.findAll();
}, },
@ -311,6 +486,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
}, },
mediafiles: function(Mediafile) { mediafiles: function(Mediafile) {
return Mediafile.findAll(); return Mediafile.findAll();
},
workflows: function(Workflow) {
return Workflow.findAll();
} }
} }
}) })
@ -351,11 +529,16 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
'$state', '$state',
'Motion', 'Motion',
'Category', 'Category',
'Tag',
'Workflow',
'User', 'User',
function($scope, $state, Motion, Category, User) { function($scope, $state, Motion, Category, Tag, Workflow, User) {
Motion.bindAll({}, $scope, 'motions'); Motion.bindAll({}, $scope, 'motions');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Tag.bindAll({}, $scope, 'tags');
Workflow.bindAll({}, $scope, 'workflows');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
$scope.alert = {};
// setup table sorting // setup table sorting
$scope.sortColumn = 'identifier'; $scope.sortColumn = 'identifier';
@ -369,6 +552,21 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
$scope.sortColumn = column; $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 // hover edit actions
$scope.hoverIn = function () { $scope.hoverIn = function () {
$scope.showEditActions = true; $scope.showEditActions = true;
@ -432,24 +630,37 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
.controller('MotionDetailCtrl', [ .controller('MotionDetailCtrl', [
'$scope', '$scope',
'$http',
'Motion', 'Motion',
'Category', 'Category',
'Workflow', 'Mediafile',
'Tag',
'User', 'User',
'Workflow',
'motion', 'motion',
'$http', function($scope, $http, Motion, Category, Mediafile, Tag, User, Workflow, motion) {
function($scope, Motion, Category, Workflow, User, motion, $http) {
Motion.bindOne(motion.id, $scope, 'motion'); Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Workflow.bindAll({}, $scope, 'workflows'); Mediafile.bindAll({}, $scope, 'mediafiles');
Tag.bindAll({}, $scope, 'tags');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Workflow.bindAll({}, $scope, 'workflows');
Motion.loadRelations(motion, 'agenda_item'); Motion.loadRelations(motion, 'agenda_item');
var state = motion.state; // TODO: make 'motion.attachments' useable and itteratable in template
state.getNextStates() // Motion.loadRelations(motion, 'attachments');
$scope.alert = {}; // TODO: show alert in template
$scope.update_state = function (state_id) { $scope.alert = {};
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': state_id}); $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) { $scope.reset_state = function (state_id) {
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {}); $http.put('/rest/motions/motion/' + motion.id + '/set_state/', {});
@ -473,22 +684,53 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
votesvalid: poll.votesvalid, votesvalid: poll.votesvalid,
votesinvalid: poll.votesinvalid, votesinvalid: poll.votesinvalid,
votescast: poll.votescast votescast: poll.votescast
}); })
.then(function(success) {
$scope.alert.show = false;
poll.isEditMode = 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', .controller('MotionCreateCtrl', [
function($scope, $state, $http, Motion, Agenda, User, Category, Workflow, Tag, Mediafile) { '$scope',
Agenda.bindAll({}, $scope, 'items'); '$state',
User.bindAll({}, $scope, 'users'); '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'); Category.bindAll({}, $scope, 'categories');
Workflow.bindAll({}, $scope, 'workflows');
Tag.bindAll({}, $scope, 'tags');
Mediafile.bindAll({}, $scope, 'mediafiles'); 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) { $scope.save = function (motion) {
Motion.create(motion).then( Motion.create(motion).then(
function(success) { function(success) {
@ -496,40 +738,68 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
} }
); );
}; };
})
}
])
.controller('MotionUpdateCtrl', [ .controller('MotionUpdateCtrl', [
'$scope', '$scope',
'$state', '$state',
'$http', 'gettext',
'Motion', 'Motion',
'Agenda',
'User',
'Category', 'Category',
'Workflow', 'Config',
'Tag',
'Mediafile', 'Mediafile',
'MotionFormFieldFactory',
'Tag',
'User',
'Workflow',
'motion', 'motion',
function ($scope, $state, $http, Motion, Agenda, User, Category, Workflow, Tag, Mediafile, motion) { function($scope, $state, gettext, Motion, Category, Config, Mediafile, MotionFormFieldFactory, Tag, User, Workflow, motion) {
Agenda.bindAll({}, $scope, 'items');
User.bindAll({}, $scope, 'users');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Workflow.bindAll({}, $scope, 'workflows');
Tag.bindAll({}, $scope, 'tags');
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
Tag.bindAll({}, $scope, 'tags');
User.bindAll({}, $scope, 'users');
Workflow.bindAll({}, $scope, 'workflows');
$scope.motion = motion; // set initial values for form model
// get latest version for edit $scope.model = motion;
$scope.motion.title = $scope.motion.getTitle(-1); $scope.model.more = false;
$scope.motion.text = $scope.motion.getText(-1); // get all form fields
$scope.motion.reason = $scope.motion.getReason(-1); $scope.formFields = MotionFormFieldFactory.getFormFields();
// override default values for update form
$scope.save = function (motion) { for (var i = 0; i < $scope.formFields.length; i++) {
Motion.save(motion).then( if ($scope.formFields[i].key == "identifier") {
function(success) { // show identifier field
$state.go('motions.motion.list'); $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> </small>
</h1> </h1>
agenda_id: {{ motion.agenda_item }} <!-- TODO: show list of speakers controls for { motion.agenda_item } -->
<br><br>state: {{ motion.state }}
<br><br>next states: {{ motion.state.getNextStates() }}
<div id="submenu"> <div id="submenu">
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default"> <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> <i class="fa fa-video-camera"></i>
</a> </a>
<!-- edit --> <!-- 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" class="btn btn-default btn-sm"
title="{{ 'Edit' | translate}}"> title="{{ 'Edit' | translate}}">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
@ -41,42 +37,73 @@ agenda_id: {{ motion.agenda_item }}
<h3 translate>Text</h3> <h3 translate>Text</h3>
<div class="white-space-pre-line" ng-bind-html="motion.getText()"></div> <div class="white-space-pre-line" ng-bind-html="motion.getText()"></div>
<!-- reason -->
<div ng-if="motion.getReason() != ''">
<h3 translate>Reason</h3> <h3 translate>Reason</h3>
<div class="white-space-pre-line" ng-bind-html="motion.getReason()"></div> <div class="white-space-pre-line" ng-bind-html="motion.getReason()"></div>
</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="col-sm-4">
<div class="well"> <div class="well">
<!-- submitters -->
<h3 translate>Submitters</h3> <h3 translate>Submitters</h3>
<div ng-repeat="submitter in motion.submitters"> <div ng-repeat="submitter in motion.submitters">
{{ submitter.get_full_name() }}<br> {{ submitter.get_full_name() }}<br>
</div> </div>
<!-- supporters -->
<div ng-if="config('motions_min_supporters') > 0"> <div ng-if="config('motions_min_supporters') > 0">
<h3 translate>Supporters</h3> <h3 translate>Supporters</h3>
<ol> <ol>
<li ng-repeat="supporters in motion.supporters"> <li ng-repeat="supporters in motion.supporters">
{{ supporters.get_full_name() }} {{ supporters.get_full_name() }}
</ol> </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> </div>
<h3 translate>State</h3> <h3 translate>State</h3>
<span class="label label-primary">{{ motion.state.name | translate }}</span> <span class="label" ng-class="'label-'+motion.state.css_class">
<button os-perms-lite="motions.can_manage" ng-click="motion.isStatusEditMode=true" {{ motion.state.name | translate }}
class="btn btn-default btn-xs"> </span>
<i class="fa fa-pencil"></i> <div ng-if="motion.allowed_actions.change_state" class="spacer">
</button>
<div ng-if="motion.isStatusEditMode" os-perms-lite="motions.can_manage">
<div class="btn-group-vertical spacer" role="group"> <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" <button ng-repeat="state in motion.state.getNextStates()" ng-click="update_state(state)"
ng-click="update_state(state_id)"> class="btn btn-default btn-sm">
State #{{state_id}} {{state.action_word}}
</button> </button>
</div> <button ng-if="motion.allowed_actions.reset_state" ng-click="reset_state()"
<div> class="btn btn-danger btn-xs">
<button ng-click="reset_state()" <i class="fa fa-exclamation-triangle"></i>
class="btn btn-danger btn-xs spacer"> <translate>Reset state</translate>
<i class="fa fa-exclamation-triangle"></i> Reset
</button> </button>
</div> </div>
</div> </div>
@ -93,8 +120,16 @@ agenda_id: {{ motion.agenda_item }}
class="btn btn-default btn-xs"> class="btn btn-default btn-xs">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
</button> </button>
<br> <div ng-show="poll.isEditMode" class="spacer">
<form ng-show="poll.isEditMode"> <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 --> <!-- yes -->
<div class="input-group col-sm-8 spacer"> <div class="input-group col-sm-8 spacer">
<div class="input-group-addon" title="{{ 'Yes' | translate }}"><i class="fa fa-thumbs-up"></i></div> <div class="input-group-addon" title="{{ 'Yes' | translate }}"><i class="fa fa-thumbs-up"></i></div>
@ -135,24 +170,34 @@ agenda_id: {{ motion.agenda_item }}
</button> </button>
</div> </div>
</form> </form>
<div ng-show="!poll.isEditMode && poll.yes"> </div>
<div ng-show="!poll.isEditMode && poll.yes >= -2">
<!-- yes --> <!-- yes -->
<div class="result_label"> <div class="result_label">
<i class="fa fa-thumbs-up"></i> <i class="fa fa-thumbs-up"></i>
<translate>Yes</translate>: <translate>Yes</translate>:
</div> </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> <div class="result_value">{{ poll.yes }} {{ poll.getYesPercent() }}</div>
<!-- no --> <!-- no -->
<div class="result_label"> <div class="result_label">
<i class="fa fa-thumbs-down"></i> <i class="fa fa-thumbs-down"></i>
<translate>No</translate>: <translate>No</translate>:
</div> </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> <div class="result_value">{{ poll.no }} {{ poll.getNoPercent() }}</div>
<!-- abstain --> <!-- abstain -->
<div class="resutl_label"> <div class="result_label">
<b>&empty;</b> <b>&empty;</b>
<translate>Abstain</translate>: <translate>Abstain</translate>:
</div> </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> <div class="result_value">{{ poll.abstain }} {{ poll.getAbstainPercent() }}</div>
<hr class="smallhr" ng-if="poll.votesvalid || poll.votesinvalid"> <hr class="smallhr" ng-if="poll.votesvalid || poll.votesinvalid">
<!-- valid votes --> <!-- valid votes -->
@ -174,7 +219,7 @@ agenda_id: {{ motion.agenda_item }}
<hr class="smallhr" ng-if="poll.votescast"> <hr class="smallhr" ng-if="poll.votescast">
<!-- votes cast --> <!-- votes cast -->
<div ng-if="poll.votescast"> <div ng-if="poll.votescast">
<div class="resutl_label"> <div class="result_label">
<b>&sum;</b> <b>&sum;</b>
<translate>Votes cast</translate>: <translate>Votes cast</translate>:
</div> </div>
@ -182,7 +227,7 @@ agenda_id: {{ motion.agenda_item }}
</div> </div>
</div> </div>
</ol> </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> <i class="fa fa-bar-chart fa-lg"></i>
<translate>New poll</translate> <translate>New poll</translate>
</button> </button>
@ -191,12 +236,11 @@ agenda_id: {{ motion.agenda_item }}
{{ motion.category.name }}</a> {{ motion.category.name }}</a>
<h3 translate>Tags</h3> <h3 translate>Tags</h3>
<span ng-repeat="tag in motion.tags"> <p ng-repeat="tag in motion.tags">
<span class="label label-default"> <span class="label label-default spacer-top">
{{ tag.name }} {{ tag.name }}
</span> </span>
&nbsp; </p>
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,94 +8,14 @@
</a> </a>
</div> </div>
<form name="motionForm"> <form name="motionForm" ng-submit="save(model)">
<div ng-if="motion.id" class="form-group"> <formly-form model="model" fields="formFields">
<label for="inputIdentifier" translate>Identifier</label> <button type="submit" ng-disabled="motionForm.$invalid" class="btn btn-primary" translate>
<input type="text" ng-model="motion.identifier" class="form-control" name="inputIdentifier"> Submit
</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
</button> </button>
<button ui-sref="motions.motion.list" class="btn btn-default" translate> <button ui-sref="motions.motion.list" class="btn btn-default" translate>
Cancel Cancel
</button> </button>
</formly-form>
</form> </form>

View File

@ -1,7 +1,7 @@
<h1 translate>Motions</h1> <h1 translate>Motions</h1>
<div id="submenu"> <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> <i class="fa fa-plus fa-lg"></i>
<translate>New</translate> <translate>New</translate>
</a> </a>
@ -37,14 +37,22 @@
<i class="fa fa-trash fa-lg"></i> <i class="fa fa-trash fa-lg"></i>
<translate>Delete selected motions</translate> <translate>Delete selected motions</translate>
</a> </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> </form>
</div> </div>
<div class="col-sm-4"> <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" <input type="text" os-focus-me ng-model="filter.search" class="form-control"
placeholder="{{ 'Filter' | translate}}"> placeholder="{{ 'Filter' | translate}}">
</div> </div>
</div> </div>
</div>
<table class="table table-striped table-bordered table-hover"> <table class="table table-striped table-bordered table-hover">
<thead> <thead>
@ -80,8 +88,9 @@
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i> </i>
<tbody> <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" orderBy: sortColumn:reverse"
class="animate-item"
ng-class="{ 'activeline': motion.isProjected(), 'selected': motion.selected }"> ng-class="{ 'activeline': motion.isProjected(), 'selected': motion.selected }">
<!-- projector column --> <!-- projector column -->
<td ng-show="!isDeleteMode" os-perms-lite="core.can_manage_projector"> <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">{{ motion.identifier }}
<td ng-if="!motion.quickEdit" ng-mouseover="motion.hover=true" ng-mouseleave="motion.hover=false"> <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> <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}"> <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> | <a ui-sref="motions.motion.detail.update({ id: motion.id })" translate>Edit</a> |
<a href="" ng-click="motion.quickEdit=true" translate>QuickEdit</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" <a href="" class="text-danger"
ng-bootbox-confirm="Are you sure you want to delete <b>{{ motion.getTitle() }}</b>?" ng-bootbox-confirm="Are you sure you want to delete <b>{{ motion.getTitle() }}</b>?"
ng-bootbox-confirm-action="deleteSingleMotion(motion)" translate>Delete</a> ng-bootbox-confirm-action="deleteSingleMotion(motion)" translate>Delete</a>
</span>
</div> </div>
<td ng-if="!motion.quickEdit" class="optional"> <td ng-if="!motion.quickEdit" class="optional">
<div ng-repeat="submitter in motion.submitters"> <div ng-repeat="submitter in motion.submitters">
@ -112,9 +128,11 @@
<td ng-if="!motion.quickEdit" class="optional"> <td ng-if="!motion.quickEdit" class="optional">
{{ motion.category.name }} {{ motion.category.name }}
<td ng-if="!motion.quickEdit" class="optional"> <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 --> <!-- 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> <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 ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
{{alert.msg}} {{alert.msg}}
@ -170,20 +188,15 @@
</ui-select> </ui-select>
</div> </div>
</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>
<div class="spacer"> <div class="spacer">
<button ng-click="motion.quickEdit=false" class="btn btn-default pull-left" translate> <button ng-click="motion.quickEdit=false" class="btn btn-default pull-left" translate>
Cancel Cancel
</button> &nbsp; </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 Update
</button> </button>
<a ui-sref="motions.motion.detail.update({id: motion.id })" class="pull-right" <a ng-if="motion.allowed_actions.update" ui-sref="motions.motion.detail.update({id: motion.id })"
translate>Edit motion...</a> class="pull-right" translate>Edit motion...</a>
</div> </div>
</table> </table>

View File

@ -66,6 +66,16 @@ class MotionViewSet(ModelViewSet):
result = False result = False
return result 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): def create(self, request, *args, **kwargs):
""" """
Customized view endpoint to create a new motion. Customized view endpoint to create a new motion.

View File

@ -9,6 +9,14 @@ angular.module('OpenSlidesApp.users', [])
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
computed: {
full_name: function () {
return this.get_full_name();
},
short_name: function () {
return this.get_short_name();
},
},
methods: { methods: {
getResourceName: function () { getResourceName: function () {
return name; return name;