diff --git a/bower.json b/bower.json
index 6026df924..eb398b69a 100644
--- a/bower.json
+++ b/bower.json
@@ -2,29 +2,30 @@
"name": "OpenSlides",
"private": true,
"dependencies": {
- "lodash": "~3.0.1",
+ "lodash": "~3.10.0",
"jquery": "~2.1.4",
"jquery.cookie": "~1.4.1",
- "bootstrap-css-only": "~3.3.4",
- "angular": "~1.3.15",
- "angular-bootstrap": "~0.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"
}
}
diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css
index e1e677347..879f6147e 100644
--- a/openslides/core/static/css/app.css
+++ b/openslides/core/static/css/app.css
@@ -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;
diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js
index 014351ca8..30a5f6ebb 100644
--- a/openslides/core/static/js/core/site.js
+++ b/openslides/core/static/js/core/site.js
@@ -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
diff --git a/openslides/core/static/templates/core/ui-select-multiple.html b/openslides/core/static/templates/core/ui-select-multiple.html
new file mode 100644
index 000000000..32cef10de
--- /dev/null
+++ b/openslides/core/static/templates/core/ui-select-multiple.html
@@ -0,0 +1,10 @@
+
+
+
+ {{$item[to.labelProp]}}
+
+
+
+
+
diff --git a/openslides/core/static/templates/core/ui-select-single.html b/openslides/core/static/templates/core/ui-select-single.html
new file mode 100644
index 000000000..efa0e4771
--- /dev/null
+++ b/openslides/core/static/templates/core/ui-select-single.html
@@ -0,0 +1,10 @@
+
+
+
+ {{$select.selected[to.labelProp]}}
+
+
+
+
+
diff --git a/openslides/motions/migrations/0004_auto_20151105_2312.py b/openslides/motions/migrations/0004_auto_20151105_2312.py
new file mode 100644
index 000000000..412d44ad6
--- /dev/null
+++ b/openslides/motions/migrations/0004_auto_20151105_2312.py
@@ -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'),
+ ),
+ ]
diff --git a/openslides/motions/models.py b/openslides/motions/models.py
index 9067f43b8..bac06ac91 100644
--- a/openslides/motions/models.py
+++ b/openslides/motions/models.py
@@ -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."""
diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py
index f9732fdac..d713288e6 100644
--- a/openslides/motions/serializers.py
+++ b/openslides/motions/serializers.py
@@ -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):
diff --git a/openslides/motions/signals.py b/openslides/motions/signals.py
index 5a61a2c4b..113113406 100644
--- a/openslides/motions/signals.py
+++ b/openslides/motions/signals.py
@@ -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
diff --git a/openslides/motions/static/js/motions/motions.js b/openslides/motions/static/js/motions/motions.js
index 41d83f393..af837ec38 100644
--- a/openslides/motions/static/js/motions/motions.js
+++ b/openslides/motions/static/js/motions/motions.js
@@ -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,64 +684,123 @@ 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 };
});
- poll.isEditMode = false;
}
}
])
-.controller('MotionCreateCtrl',
- function($scope, $state, $http, Motion, Agenda, User, Category, Workflow, Tag, Mediafile) {
- Agenda.bindAll({}, $scope, 'items');
- User.bindAll({}, $scope, 'users');
- Category.bindAll({}, $scope, 'categories');
- Workflow.bindAll({}, $scope, 'workflows');
- Tag.bindAll({}, $scope, 'tags');
- Mediafile.bindAll({}, $scope, 'mediafiles');
-
- $scope.motion = {};
- $scope.save = function (motion) {
- Motion.create(motion).then(
- function(success) {
- $state.go('motions.motion.list');
- }
- );
- };
-})
-
-.controller('MotionUpdateCtrl', [
+.controller('MotionCreateCtrl', [
'$scope',
'$state',
- '$http',
+ 'gettext',
'Motion',
- 'Agenda',
- 'User',
+ 'MotionFormFieldFactory',
'Category',
- 'Workflow',
- 'Tag',
+ 'Config',
'Mediafile',
- 'motion',
- function ($scope, $state, $http, Motion, Agenda, User, Category, Workflow, Tag, Mediafile, motion) {
- Agenda.bindAll({}, $scope, 'items');
- User.bindAll({}, $scope, 'users');
+ '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 = 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);
-
+ // get all form fields
+ $scope.formFields = MotionFormFieldFactory.getFormFields();
+ // override default values for create form
+ for (var i = 0; i < $scope.formFields.length; i++) {
+ if ($scope.formFields[i].key == "identifier") {
+ $scope.formFields[i].hide = true;
+ }
+ if ($scope.formFields[i].key == "workflow_id") {
+ // preselect default workflow
+ $scope.formFields[i].defaultValue = Config.get('motions_workflow').value;
+ }
+ }
$scope.save = function (motion) {
- Motion.save(motion).then(
+ Motion.create(motion).then(
function(success) {
$state.go('motions.motion.list');
}
);
};
+
+ }
+])
+
+.controller('MotionUpdateCtrl', [
+ '$scope',
+ '$state',
+ 'gettext',
+ 'Motion',
+ 'Category',
+ 'Config',
+ 'Mediafile',
+ 'MotionFormFieldFactory',
+ 'Tag',
+ 'User',
+ 'Workflow',
+ 'motion',
+ function($scope, $state, gettext, Motion, Category, Config, Mediafile, MotionFormFieldFactory, Tag, User, Workflow, motion) {
+ Category.bindAll({}, $scope, 'categories');
+ Mediafile.bindAll({}, $scope, 'mediafiles');
+ Tag.bindAll({}, $scope, 'tags');
+ User.bindAll({}, $scope, 'users');
+ Workflow.bindAll({}, $scope, 'workflows');
+
+ // set initial values for form model
+ $scope.model = motion;
+ $scope.model.more = false;
+ // get all form fields
+ $scope.formFields = MotionFormFieldFactory.getFormFields();
+ // override default values for update form
+ for (var i = 0; i < $scope.formFields.length; i++) {
+ if ($scope.formFields[i].key == "identifier") {
+ // show identifier field
+ $scope.formFields[i].hide = false;
+ }
+ if ($scope.formFields[i].key == "title") {
+ // get title of latest version
+ $scope.formFields[i].defaultValue = motion.getTitle(-1);
+ }
+ if ($scope.formFields[i].key == "text") {
+ // get text of latest version
+ $scope.formFields[i].defaultValue = motion.getText(-1);
+ }
+ if ($scope.formFields[i].key == "reason") {
+ // get reason of latest version
+ $scope.formFields[i].defaultValue = motion.getReason(-1);
+ }
+ if ($scope.formFields[i].key == "workflow_id") {
+ // get saved workflow id from state
+ $scope.formFields[i].defaultValue = motion.state.workflow_id;
+ }
+ }
+
+ // save form
+ $scope.save = function (model) {
+ Motion.save(model)
+ .then(function(success) {
+ $state.go('motions.motion.detail', {id: motion.id});
+ })
+ .catch(function(fallback) {
+ console.log(fallback);
+ });
+ };
}
])
diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html
index 9cc830743..026610707 100644
--- a/openslides/motions/static/templates/motions/motion-detail.html
+++ b/openslides/motions/static/templates/motions/motion-detail.html
@@ -6,11 +6,7 @@
-agenda_id: {{ motion.agenda_item }}
-
-
state: {{ motion.state }}
-
-
next states: {{ motion.state.getNextStates() }}
+
+
Submitters
{{ submitter.get_full_name() }}
+
Supporters
-
- {{ supporters.get_full_name() }}
+ {{ supporters.get_full_name() }}
+
+
+
+
State
-
{{ motion.state.name | translate }}
-
-
+
+ {{ motion.state.name | translate }}
+
+
-
-
-
- Reset
-
@@ -93,67 +120,85 @@ agenda_id: {{ motion.agenda_item }}
class="btn btn-default btn-xs">
-
-
-
+
+
Yes:
-
{{ poll.yes }} {{poll.getYesPercent()}}
+
+
+
+
{{ poll.yes }} {{ poll.getYesPercent() }}
No:
-
{{ poll.no }} {{poll.getNoPercent()}}
+
+
+
+
{{ poll.no }} {{ poll.getNoPercent() }}
-
+
∅
Abstain:
-
{{ poll.abstain }} {{poll.getAbstainPercent()}}
+
+
+
+
{{ poll.abstain }} {{ poll.getAbstainPercent() }}
@@ -161,7 +206,7 @@ agenda_id: {{ motion.agenda_item }}
Valid votes:
-
{{ poll.votesvalid }} {{poll.getVotesValidPercent()}}
+
{{ poll.votesvalid }} {{ poll.getVotesValidPercent() }}
@@ -169,20 +214,20 @@ agenda_id: {{ motion.agenda_item }}
Invalid votes:
-
{{ poll.votesinvalid }} {{poll.getVotesInvalidPercent()}}
+
{{ poll.votesinvalid }} {{ poll.getVotesInvalidPercent() }}
-
+
∑
Votes cast:
-
{{ poll.votescast }} {{poll.getVotesCastPercent()}}
+
{{ poll.votescast }} {{ poll.getVotesCastPercent() }}
-
+
New poll
@@ -191,12 +236,11 @@ agenda_id: {{ motion.agenda_item }}
{{ motion.category.name }}
Tags
-
-
+
+
{{ tag.name }}
-
-
+
diff --git a/openslides/motions/static/templates/motions/motion-form.html b/openslides/motions/static/templates/motions/motion-form.html
index 07b64f0d2..5fb3f33ed 100644
--- a/openslides/motions/static/templates/motions/motion-form.html
+++ b/openslides/motions/static/templates/motions/motion-form.html
@@ -8,94 +8,14 @@
-
+
diff --git a/openslides/motions/static/templates/motions/motion-list.html b/openslides/motions/static/templates/motions/motion-list.html
index 2fad69694..6709c935f 100644
--- a/openslides/motions/static/templates/motions/motion-list.html
+++ b/openslides/motions/static/templates/motions/motion-list.html
@@ -1,7 +1,7 @@
Motions
@@ -80,8 +88,9 @@
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
-
@@ -98,12 +107,19 @@
| {{ motion.identifier }}
|
{{ motion.getTitle() }}
-
- Edit |
- QuickEdit |
- Delete
+
@@ -112,9 +128,11 @@
{{ motion.category.name }}
|
- {{ motion.state.name | translate }}
+
+ {{ motion.state.name | translate }}
+
- |
+ |
{{ motion.getTitle() }} – Quick Edit
{{alert.msg}}
@@ -170,20 +188,15 @@
-
-
-
-
diff --git a/openslides/motions/views.py b/openslides/motions/views.py
index 3dac54d8e..2dc66d248 100644
--- a/openslides/motions/views.py
+++ b/openslides/motions/views.py
@@ -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.
diff --git a/openslides/users/static/js/users/base.js b/openslides/users/static/js/users/base.js
index 0fea9bfb7..c683f38fc 100644
--- a/openslides/users/static/js/users/base.js
+++ b/openslides/users/static/js/users/base.js
@@ -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;
| | |