Custom workflows and states:
- Added new workflow list view - Added state table for each workflow - Added new StateViewSet to handle states of workflows
This commit is contained in:
parent
56a7bd6840
commit
9e4cafd0f0
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
*.pyc
|
||||
*.swp
|
||||
*.swo
|
||||
*.log
|
||||
*~
|
||||
|
||||
# Virtual Environment
|
||||
|
@ -12,6 +12,7 @@ Motions:
|
||||
- New possibility to sort submitters [#3647].
|
||||
- New representation of amendments (paragraph based creation, new diff
|
||||
and list views for amendments) [#3637].
|
||||
- New feature to customize workflows and states [#3772].
|
||||
|
||||
|
||||
Version 2.2 (2018-06-06)
|
||||
|
@ -30,15 +30,18 @@
|
||||
|
||||
#nav .navbar li a { padding: 24px 5px; }
|
||||
|
||||
#groups-table .perm-head {
|
||||
#multi-table .info-head {
|
||||
width: 200px;
|
||||
&.small {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
/* hide text in groups-table earlier */
|
||||
#groups-table .optional { display: none; }
|
||||
/* hide text in multi-table earlier */
|
||||
#multi-table .optional { display: none; }
|
||||
|
||||
/* show replacement elements, if any */
|
||||
#groups-table .optional-show { display: block !important; }
|
||||
#multi-table .optional-show { display: block !important; }
|
||||
|
||||
/* hide searchbar input */
|
||||
#nav .searchbar input { display: none !important; }
|
||||
@ -99,8 +102,11 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#groups-table .perm-head {
|
||||
#multi-table .info-head {
|
||||
width: 150px;
|
||||
&.small {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.personalNoteFixed {
|
||||
|
47
openslides/core/static/css/core/_multi-table.scss
Normal file
47
openslides/core/static/css/core/_multi-table.scss
Normal file
@ -0,0 +1,47 @@
|
||||
/* multi list */
|
||||
#multi-table {
|
||||
table-layout: fixed;
|
||||
text-align: center;
|
||||
|
||||
thead tr th {
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info-head {
|
||||
width: 300px;
|
||||
&.small {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
tbody tr.bg-grey {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
tbody tr td .no-overflow{
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
tbody tr td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tbody tr td div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.optional-show { /* hide optional-show column */
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editable-click {
|
||||
color: #000;
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
@import "config";
|
||||
@import "search";
|
||||
@import "os-table";
|
||||
@import "multi-table";
|
||||
@import "csv-import";
|
||||
@import "chatbox";
|
||||
@import "countdown";
|
||||
|
@ -29,6 +29,7 @@ class MotionsAppConfig(AppConfig):
|
||||
MotionBlockViewSet,
|
||||
MotionPollViewSet,
|
||||
MotionChangeRecommendationViewSet,
|
||||
StateViewSet,
|
||||
WorkflowViewSet,
|
||||
)
|
||||
|
||||
@ -55,6 +56,7 @@ class MotionsAppConfig(AppConfig):
|
||||
router.register(self.get_model('MotionChangeRecommendation').get_collection_string(),
|
||||
MotionChangeRecommendationViewSet)
|
||||
router.register(self.get_model('MotionPoll').get_collection_string(), MotionPollViewSet)
|
||||
router.register(self.get_model('State').get_collection_string(), StateViewSet)
|
||||
|
||||
def get_startup_elements(self):
|
||||
"""
|
||||
|
45
openslides/motions/migrations/0008_auto_20180702_1128.py
Normal file
45
openslides/motions/migrations/0008_auto_20180702_1128.py
Normal file
@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.8 on 2018-07-02 09:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0007_motionversion_amendment_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='workflow',
|
||||
name='first_state',
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='motions.State'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='motion',
|
||||
name='state',
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='motions.State'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='state',
|
||||
name='next_states',
|
||||
field=models.ManyToManyField(blank=True, to='motions.State'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='state',
|
||||
name='action_word',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
@ -83,7 +83,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
state = models.ForeignKey(
|
||||
'State',
|
||||
related_name='+',
|
||||
on_delete=models.SET_NULL,
|
||||
on_delete=models.PROTECT, # Do not let the user delete states, that are used for motions
|
||||
null=True) # TODO: Check whether null=True is necessary.
|
||||
"""
|
||||
The related state object.
|
||||
@ -1196,7 +1196,7 @@ class State(RESTModelMixin, models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
"""A string representing the state."""
|
||||
|
||||
action_word = models.CharField(max_length=255)
|
||||
action_word = models.CharField(max_length=255, blank=True)
|
||||
"""An alternative string to be used for a button to switch to this state."""
|
||||
|
||||
recommendation_label = models.CharField(max_length=255, null=True)
|
||||
@ -1208,7 +1208,7 @@ class State(RESTModelMixin, models.Model):
|
||||
related_name='states')
|
||||
"""A many-to-one relation to a workflow."""
|
||||
|
||||
next_states = models.ManyToManyField('self', symmetrical=False)
|
||||
next_states = models.ManyToManyField('self', symmetrical=False, blank=True)
|
||||
"""A many-to-many relation to all states, that can be choosen from this state."""
|
||||
|
||||
css_class = models.CharField(max_length=255, default='primary')
|
||||
@ -1338,7 +1338,8 @@ class Workflow(RESTModelMixin, models.Model):
|
||||
State,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
null=True)
|
||||
null=True,
|
||||
blank=True)
|
||||
"""A one-to-one relation to a state, the starting point for the workflow."""
|
||||
|
||||
class Meta:
|
||||
|
@ -101,12 +101,43 @@ class WorkflowSerializer(ModelSerializer):
|
||||
Serializer for motion.models.Workflow objects.
|
||||
"""
|
||||
states = StateSerializer(many=True, read_only=True)
|
||||
first_state = PrimaryKeyRelatedField(read_only=True)
|
||||
# The first_state is checked in the update() method
|
||||
first_state = PrimaryKeyRelatedField(queryset=State.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = ('id', 'name', 'states', 'first_state',)
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Customized create method. Creating a new workflow does always create a
|
||||
new state which is used as first state.
|
||||
"""
|
||||
workflow = super().create(validated_data)
|
||||
first_state = State.objects.create(
|
||||
name='new',
|
||||
action_word='new',
|
||||
workflow=workflow,
|
||||
allow_create_poll=True,
|
||||
allow_support=True,
|
||||
allow_submitter_edit=True
|
||||
)
|
||||
workflow.first_state = first_state
|
||||
workflow.save()
|
||||
return workflow
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, workflow, validated_data):
|
||||
"""
|
||||
Check, if the first state is in the right workflow.
|
||||
"""
|
||||
first_state = validated_data.get('first_state')
|
||||
if first_state is not None:
|
||||
if workflow.pk != first_state.workflow.pk:
|
||||
raise ValidationError({'detail': 'You cannot select a state which is not in the workflow as the first state.'})
|
||||
return super().update(workflow, validated_data)
|
||||
|
||||
|
||||
class MotionCommentsJSONSerializerField(Field):
|
||||
"""
|
||||
|
@ -10,19 +10,16 @@ angular.module('OpenSlidesApp.motions', [
|
||||
'OpenSlidesApp.users',
|
||||
])
|
||||
|
||||
.factory('WorkflowState', [
|
||||
.factory('MotionState', [
|
||||
'DS',
|
||||
function (DS) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/workflowstate',
|
||||
name: 'motions/state',
|
||||
methods: {
|
||||
getNextStates: function () {
|
||||
// TODO: Use filter with params with operator 'in'.
|
||||
var states = [];
|
||||
_.forEach(this.next_states_id, function (stateId) {
|
||||
states.push(DS.get('motions/workflowstate', stateId));
|
||||
return _.map(this.next_states_id, function (stateId) {
|
||||
return DS.get('motions/state', stateId);
|
||||
});
|
||||
return states;
|
||||
},
|
||||
getRecommendations: function () {
|
||||
var params = {
|
||||
@ -35,22 +32,34 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
}
|
||||
};
|
||||
return DS.filter('motions/workflowstate', params);
|
||||
return DS.filter('motions/state', params);
|
||||
}
|
||||
}
|
||||
},
|
||||
relations: {
|
||||
hasOne: {
|
||||
'motions/workflow': {
|
||||
localField: 'workflow',
|
||||
localKey: 'workflow_id',
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.factory('Workflow', [
|
||||
'DS',
|
||||
'WorkflowState',
|
||||
function (DS, WorkflowState) {
|
||||
function (DS) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/workflow',
|
||||
methods: {
|
||||
getFirstState: function () {
|
||||
return DS.get('motions/state', this.first_state);
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
hasMany: {
|
||||
'motions/workflowstate': {
|
||||
'motions/state': {
|
||||
localField: 'states',
|
||||
foreignKey: 'workflow_id',
|
||||
}
|
||||
@ -1212,7 +1221,7 @@ angular.module('OpenSlidesApp.motions', [
|
||||
},
|
||||
},
|
||||
hasOne: {
|
||||
'motions/workflowstate': [
|
||||
'motions/state': [
|
||||
{
|
||||
localField: 'state',
|
||||
localKey: 'state_id',
|
||||
@ -1523,9 +1532,10 @@ angular.module('OpenSlidesApp.motions', [
|
||||
'Motion',
|
||||
'Category',
|
||||
'Workflow',
|
||||
'MotionState',
|
||||
'MotionChangeRecommendation',
|
||||
'Submitter',
|
||||
function(Motion, Category, Workflow, MotionChangeRecommendation, Submitter) {}
|
||||
function(Motion, Category, Workflow, MotionState, MotionChangeRecommendation, Submitter) {}
|
||||
])
|
||||
|
||||
|
||||
|
@ -10,6 +10,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'OpenSlidesApp.motions.docx',
|
||||
'OpenSlidesApp.motions.pdf',
|
||||
'OpenSlidesApp.motions.csv',
|
||||
'OpenSlidesApp.motions.workflow',
|
||||
])
|
||||
|
||||
.config([
|
||||
@ -188,6 +189,24 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
});
|
||||
}
|
||||
],
|
||||
})
|
||||
// Workflows and states
|
||||
.state('motions.workflow', {
|
||||
url: '/workflow',
|
||||
abstract: true,
|
||||
template: '<ui-view/>',
|
||||
data: {
|
||||
title: gettext('Workflows'),
|
||||
basePerm: 'motions.can_manage',
|
||||
},
|
||||
})
|
||||
.state('motions.workflow.list', {})
|
||||
.state('motions.workflow.detail', {
|
||||
resolve: {
|
||||
workflowId: ['$stateParams', function($stateParams) {
|
||||
return $stateParams.id;
|
||||
}],
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
@ -2216,8 +2235,21 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.model.motion_block_id = parentMotion.motion_block_id;
|
||||
Motion.bindOne($scope.model.parent_id, $scope, 'parent');
|
||||
}
|
||||
// ... preselect default workflow
|
||||
$scope.model.workflow_id = Config.get('motions_workflow').value;
|
||||
// ... preselect default workflow if exist
|
||||
var workflow = Workflow.get(Config.get('motions_workflow').value);
|
||||
if (!workflow) {
|
||||
workflow = _.first(Workflow.getAll());
|
||||
}
|
||||
if (workflow) {
|
||||
$scope.model.workflow_id = workflow.id;
|
||||
} else {
|
||||
$scope.alert = {
|
||||
type: 'danger',
|
||||
msg: gettextCatalog.getString('No workflows exists. You will not ' +
|
||||
'be able to create a motion.'),
|
||||
show: true,
|
||||
};
|
||||
}
|
||||
|
||||
// get all form fields
|
||||
$scope.formFields = MotionForm.getFormFields(true, isParagraphBasedAmendment);
|
||||
|
257
openslides/motions/static/js/motions/workflow.js
Normal file
257
openslides/motions/static/js/motions/workflow.js
Normal file
@ -0,0 +1,257 @@
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('OpenSlidesApp.motions.workflow', [])
|
||||
|
||||
.controller('WorkflowListCtrl', [
|
||||
'$scope',
|
||||
'Workflow',
|
||||
'ngDialog',
|
||||
'ErrorMessage',
|
||||
function ($scope, Workflow, ngDialog, ErrorMessage) {
|
||||
$scope.alert = {};
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
$scope.create = function () {
|
||||
ngDialog.open({
|
||||
template: 'static/templates/motions/workflow-edit.html',
|
||||
controller: 'WorkflowCreateCtrl',
|
||||
className: 'ngdialog-theme-default wide-form',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
});
|
||||
};
|
||||
$scope.delete = function (workflow) {
|
||||
Workflow.destroy(workflow).then(null, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('WorkflowDetailCtrl', [
|
||||
'$scope',
|
||||
'$sessionStorage',
|
||||
'permissions',
|
||||
'Workflow',
|
||||
'MotionState',
|
||||
'workflowId',
|
||||
'ngDialog',
|
||||
'gettextCatalog',
|
||||
'ErrorMessage',
|
||||
function ($scope, $sessionStorage, permissions, Workflow, MotionState, workflowId,
|
||||
ngDialog, gettextCatalog, ErrorMessage) {
|
||||
$scope.permissions = permissions;
|
||||
$scope.alert = {};
|
||||
|
||||
$scope.$watch(function () {
|
||||
return Workflow.lastModified(workflowId);
|
||||
}, function () {
|
||||
$scope.workflow = Workflow.get(workflowId);
|
||||
_.forEach($scope.workflow.states, function (state) {
|
||||
state.newActionWord = gettextCatalog.getString(state.action_word);
|
||||
state.newRecommendationLabel = gettextCatalog.getString(state.recommendation_label);
|
||||
});
|
||||
});
|
||||
|
||||
$scope.booleanMembers = [
|
||||
{name: 'allow_support',
|
||||
displayName: 'Allow support',},
|
||||
{name: 'allow_create_poll',
|
||||
displayName: 'Allow create poll',},
|
||||
{name: 'allow_submitter_edit',
|
||||
displayName: 'Allow submitter edit',},
|
||||
{name: 'versioning',
|
||||
displayName: 'Versioning',},
|
||||
{name: 'leave_old_version_active',
|
||||
displayName: 'Leave old version active',},
|
||||
{name: 'dont_set_identifier',
|
||||
displayName: 'Set identifier',
|
||||
inverse: true,},
|
||||
{name: 'show_state_extension_field',
|
||||
displayName: 'Show state extension field',},
|
||||
{name: 'show_recommendation_extension_field',
|
||||
displayName: 'Show recommendation extension field',}
|
||||
];
|
||||
$scope.cssClasses = {
|
||||
'danger': 'Red',
|
||||
'success': 'Green',
|
||||
'warning': 'Yellow',
|
||||
'default': 'Grey',
|
||||
'primary': 'Blue',
|
||||
};
|
||||
$scope.getPermissionDisplayName = function (permission) {
|
||||
if (permission) {
|
||||
return _.find($scope.permissions, function (perm) {
|
||||
return perm.value === permission;
|
||||
}).display_name;
|
||||
}
|
||||
};
|
||||
$scope.clickPermission = function (state, permission) {
|
||||
state.required_permission_to_see =
|
||||
state.required_permission_to_see === permission.value ? '' : permission.value;
|
||||
$scope.save(state);
|
||||
};
|
||||
$scope.xor = function (a, b) {
|
||||
return (a && !b) || (!a && b);
|
||||
};
|
||||
|
||||
$scope.changeBooleanMember = function (state, memberName) {
|
||||
state[memberName] = !state[memberName];
|
||||
$scope.save(state);
|
||||
};
|
||||
$scope.setMember = function (state, member, value) {
|
||||
state[member] = value;
|
||||
$scope.save(state);
|
||||
};
|
||||
$scope.clickNextStateEntry = function (state, clickedStateId) {
|
||||
var index = state.next_states_id.indexOf(clickedStateId);
|
||||
if (index > -1) { // remove now
|
||||
state.next_states_id.splice(index, 1);
|
||||
} else { // add
|
||||
state.next_states_id.push(clickedStateId);
|
||||
}
|
||||
$scope.save(state);
|
||||
};
|
||||
$scope.save = function (state) {
|
||||
MotionState.save(state).then(null, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setFirstState = function (state) {
|
||||
$scope.workflow.first_state = state.id;
|
||||
Workflow.save($scope.workflow).then(null, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
|
||||
// Save expand state so the session
|
||||
if ($sessionStorage.motionStateTableExpandState) {
|
||||
$scope.toggleExpandContent();
|
||||
}
|
||||
$scope.saveExpandState = function (state) {
|
||||
$sessionStorage.motionStateTableExpandState = state;
|
||||
};
|
||||
|
||||
$scope.openStateDialog = function (state) {
|
||||
ngDialog.open({
|
||||
template: 'static/templates/motions/state-edit.html',
|
||||
controller: state ? 'StateRenameCtrl' : 'StateCreateCtrl',
|
||||
className: 'ngdialog-theme-default wide-form',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
state: function () {return state;},
|
||||
workflow: function () {return $scope.workflow;},
|
||||
}
|
||||
});
|
||||
};
|
||||
$scope.openWorkflowDialog = function () {
|
||||
ngDialog.open({
|
||||
template: 'static/templates/motions/workflow-edit.html',
|
||||
controller: 'WorkflowRenameCtrl',
|
||||
className: 'ngdialog-theme-default wide-form',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
workflow: function () {return $scope.workflow;},
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.delete = function (state) {
|
||||
MotionState.destroy(state).then(null, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('WorkflowCreateCtrl', [
|
||||
'$scope',
|
||||
'Workflow',
|
||||
'ErrorMessage',
|
||||
function ($scope, Workflow, ErrorMessage) {
|
||||
$scope.save = function () {
|
||||
var workflow = {
|
||||
name: $scope.newName,
|
||||
};
|
||||
Workflow.create(workflow).then(function (success) {
|
||||
$scope.closeThisDialog();
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('WorkflowRenameCtrl', [
|
||||
'$scope',
|
||||
'workflow',
|
||||
'Workflow',
|
||||
'gettextCatalog',
|
||||
'ErrorMessage',
|
||||
function ($scope, workflow, Workflow, gettextCatalog, ErrorMessage) {
|
||||
$scope.workflow = workflow;
|
||||
$scope.newName = gettextCatalog.getString(workflow.name);
|
||||
$scope.save = function () {
|
||||
workflow.name = $scope.newName;
|
||||
Workflow.save(workflow).then(function (success) {
|
||||
$scope.closeThisDialog();
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('StateCreateCtrl', [
|
||||
'$scope',
|
||||
'workflow',
|
||||
'MotionState',
|
||||
'ErrorMessage',
|
||||
function ($scope, workflow, MotionState, ErrorMessage) {
|
||||
$scope.newName = '';
|
||||
$scope.actionWord = '';
|
||||
$scope.save = function () {
|
||||
var state = {
|
||||
name: $scope.newName,
|
||||
action_word: $scope.actionWord,
|
||||
workflow_id: workflow.id,
|
||||
allow_create_poll: true,
|
||||
allow_support: true,
|
||||
allow_submitter_edit: true,
|
||||
};
|
||||
MotionState.create(state).then(function () {
|
||||
$scope.closeThisDialog();
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
.controller('StateRenameCtrl', [
|
||||
'$scope',
|
||||
'MotionState',
|
||||
'state',
|
||||
'gettextCatalog',
|
||||
'ErrorMessage',
|
||||
function ($scope, MotionState, state, gettextCatalog, ErrorMessage) {
|
||||
$scope.state = state;
|
||||
$scope.newName = gettextCatalog.getString(state.name);
|
||||
$scope.actionWord = gettextCatalog.getString(state.action_word);
|
||||
$scope.save = function () {
|
||||
state.name = $scope.newName;
|
||||
state.action_word = $scope.actionWord;
|
||||
MotionState.save(state).then(function () {
|
||||
$scope.closeThisDialog();
|
||||
}, function (error) {
|
||||
$scope.alert = ErrorMessage.forAlert(error);
|
||||
});
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
}());
|
@ -41,9 +41,9 @@
|
||||
<strong>{{ category.name }}</strong>
|
||||
<div class="hoverActions" ng-class="{'hiddenDiv': !category.hover}">
|
||||
<!-- sort -->
|
||||
<a ui-sref="motions.category.sort({ id: category.id })" translate>Sort</a> |
|
||||
<a ui-sref="motions.category.sort({ id: category.id })" translate>Sort</a> ·
|
||||
<!-- edit -->
|
||||
<a href="" ng-click="editOrCreate(category)" translate>Edit</a> |
|
||||
<a href="" ng-click="editOrCreate(category)" translate>Edit</a> ·
|
||||
<!-- delete -->
|
||||
<a href="" class="text-danger"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||
|
@ -17,6 +17,10 @@
|
||||
<i class="fa fa-th-large fa-lg"></i>
|
||||
<translate>Motion blocks</translate>
|
||||
</a>
|
||||
<a ui-sref="motions.workflow.list" os-perms="motions.can_manage" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-code-fork fa-lg"></i>
|
||||
<translate>Workflows</translate>
|
||||
</a>
|
||||
<a ui-sref="core.tag.list" os-perms="core.can_manage_tags" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-tags fa-lg"></i>
|
||||
<translate>Tags</translate>
|
||||
|
28
openslides/motions/static/templates/motions/state-edit.html
Normal file
28
openslides/motions/static/templates/motions/state-edit.html
Normal file
@ -0,0 +1,28 @@
|
||||
<h1 ng-if="state" translate>Edit state</h1>
|
||||
<h1 ng-if="!state" translate>Create new state</h1>
|
||||
<div uib-alert ng-show="alert.show" class="alert-danger" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
|
||||
<form name="stateForm" ng-submit="save()">
|
||||
<label for="name" translate>
|
||||
Name
|
||||
</label>
|
||||
<div>
|
||||
<input class="form-control" id="name" type="text" ng-model="newName">
|
||||
</div>
|
||||
<label for="actionword" class="spacer-top" translate>
|
||||
Action word
|
||||
</label>
|
||||
<div>
|
||||
<input class="form-control" id="actionword" type="text" ng-model="actionWord">
|
||||
</div>
|
||||
<div class="spacer-top-lg">
|
||||
<button type="submit" ng-disabled="newName == ''" class="btn btn-primary" translate>
|
||||
Save
|
||||
</button>
|
||||
<button ng-click="closeThisDialog()" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
212
openslides/motions/static/templates/motions/workflow-detail.html
Normal file
212
openslides/motions/static/templates/motions/workflow-detail.html
Normal file
@ -0,0 +1,212 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="submenu">
|
||||
<a ui-sref="motions.workflow.list" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||
<translate>Back to overview</translate>
|
||||
</a>
|
||||
<a os-perms="motions.can_manage" class="btn btn-primary btn-sm"
|
||||
ng-click="openStateDialog()" title="add new state">
|
||||
<i class="fa fa-plus fa-lg"></i>
|
||||
<translate>New</translate>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm"
|
||||
ng-class="expandContent ? 'btn-primary' : 'btn-default'"
|
||||
ng-click="toggleExpandContent(); saveExpandState(expandContent)">
|
||||
<i class="fa fa-arrows-h fa-lg"></i>
|
||||
<span ng-if="!expandContent" translate>Expand</span>
|
||||
<span ng-if="expandContent" translate>Reduce</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1>
|
||||
{{ workflow.name | translate }}
|
||||
<i class="fa fa-pencil pointer" ng-click="openWorkflowDialog()"></i>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="title">
|
||||
<h3 ng-mouseover="firstStateHover=true" ng-mouseleave="firstStateHover=false">
|
||||
<translate>First state</translate>:
|
||||
{{ workflow.getFirstState().name | translate }}
|
||||
<span uib-dropdown>
|
||||
<span id="firstStateDropdown" class="pointer" uib-dropdown-toggle>
|
||||
<i class="fa fa-cog" ng-if="firstStateHover"></i>
|
||||
</span>
|
||||
<ul class="dropdown-menu" aria-labelledby="firstStateDropdown">
|
||||
<li ng-repeat="state in workflow.states">
|
||||
<a href ng-click="setFirstState(state)">
|
||||
<i class="fa fa-check" ng-if="workflow.first_state === state.id"></i>
|
||||
{{ state.name | translate }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div uib-alert ng-show="alert.show" class="alert-danger" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
<table id="multi-table" class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-head small">
|
||||
<h4 translate>Permissions</h4>
|
||||
<th ng-repeat="state in workflow.states" ng-mouseover="thHover=true" ng-mouseleave="thHover=false">
|
||||
<span class="optional">
|
||||
{{ state.name | translate }}
|
||||
</span>
|
||||
<span class="optional-show" uib-tooltip="{{ state.name | translate }}">
|
||||
{{ state.name | translate | limitTo: 1 }}...
|
||||
</span>
|
||||
<div os-perms="motions.can_manage" class="hoverActions text-center"
|
||||
ng-class="{'hiddenDiv': !thHover}">
|
||||
<!--edit name-->
|
||||
<a href="" ng-click="openStateDialog(state)">
|
||||
<i class="fa fa-pencil fa-lg"></i></a>
|
||||
|
||||
<!--delete-->
|
||||
<a href="" class="text-danger" ng-if="state.id !== workflow.first_state"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||
<b>{{ state.name | translate }}</b>"
|
||||
ng-bootbox-confirm-action="delete(state)">
|
||||
<i class="fa fa-trash fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<b translate>Action word</b>
|
||||
</td>
|
||||
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
|
||||
<div class="popover-wrapper">
|
||||
<span editable-text="state.newActionWord"
|
||||
onaftersave="setMember(state, 'action_word', state.newActionWord)">
|
||||
<div class="no-overflow" ng-if="state.action_word">
|
||||
{{ state.action_word | translate }}
|
||||
</div>
|
||||
<span class="text-muted" ng-if="!state.action_word">
|
||||
—
|
||||
</span>
|
||||
<i class="fa fa-pencil" ng-if="tdHover"></i>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<b translate>Recommendation label</b>
|
||||
</td>
|
||||
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
|
||||
<div class="popover-wrapper">
|
||||
<span editable-text="state.newRecommendationLabel"
|
||||
onaftersave="setMember(state, 'recommendation_label', state.newRecommendationLabel)">
|
||||
<div class="no-overflow" ng-if="state.recommendation_label">
|
||||
{{ state.recommendation_label | translate }}
|
||||
</div>
|
||||
<span class="text-muted" ng-if="!state.recommendation_label">
|
||||
—
|
||||
</span>
|
||||
<i class="fa fa-pencil" ng-if="tdHover"></i>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="member in booleanMembers">
|
||||
<td>
|
||||
<b>{{ member.displayName | translate }}</b>
|
||||
</td>
|
||||
<td ng-repeat="state in workflow.states" class="pointer"
|
||||
ng-click="changeBooleanMember(state, member.name)">
|
||||
<!-- Simulating a checkbox with FontAwesome icons. -->
|
||||
<i class="fa"
|
||||
ng-class="xor(state[member.name], member.inverse) ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<b translate>Label color</b>
|
||||
</td>
|
||||
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
|
||||
<span uib-dropdown>
|
||||
<span id="dropdownCssClass{{ state.id }}" class="pointer" uib-dropdown-toggle>
|
||||
<span class="label" ng-class="'label-' + state.css_class">
|
||||
{{ cssClasses[state.css_class] }}
|
||||
</span>
|
||||
<i class="fa fa-cog" ng-if="tdHover"></i>
|
||||
</span>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownCssClass{{ state.id }}">
|
||||
<li ng-repeat="(class, name) in cssClasses">
|
||||
<a href ng-click="setMember(state, 'css_class', class)">
|
||||
<i class="fa fa-check" ng-if="state.css_class === class"></i>
|
||||
{{ name | translate }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<b translate>Required permission to see</b>
|
||||
</td>
|
||||
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
|
||||
<span uib-dropdown>
|
||||
<span id="dropdownPermission{{ state.id }}" class="pointer" uib-dropdown-toggle>
|
||||
<div class="no-overflow">
|
||||
<span ng-if="state.required_permission_to_see">
|
||||
{{ getPermissionDisplayName(state.required_permission_to_see) | translate }}
|
||||
</span>
|
||||
<span class="text-muted" ng-if="!state.required_permission_to_see">
|
||||
—
|
||||
</span>
|
||||
<i class="fa fa-cog" ng-if="tdHover"></i>
|
||||
</div>
|
||||
</span>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownPermission{{ state.id }}">
|
||||
<li ng-repeat="permission in permissions">
|
||||
<a href ng-click="clickPermission(state, permission)">
|
||||
<i class="fa fa-check" ng-if="state.required_permission_to_see === permission.value"></i>
|
||||
{{ permission.display_name | translate }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<b translate>Next states</b>
|
||||
</td>
|
||||
<td ng-repeat="state in workflow.states" ng-mouseover="tdHover=true" ng-mouseleave="tdHover=false">
|
||||
<span ng-if="state.getNextStates().length === 0" class="text-muted" translate>
|
||||
—
|
||||
</span>
|
||||
<div class="no-overflow">
|
||||
<span ng-repeat="nextState in state.getNextStates()">
|
||||
{{ nextState.name | translate }}<span ng-if="!$last">,</br></span>
|
||||
</span>
|
||||
</div>
|
||||
<span uib-dropdown>
|
||||
<span id="dropdownNextStates{{ state.id }}" class="pointer"
|
||||
uib-dropdown-toggle>
|
||||
<i class="fa fa-cog" ng-if="tdHover"></i>
|
||||
</span>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownNextStates{{ state.id }}">
|
||||
<li ng-repeat="s in workflow.states">
|
||||
<a href ng-click="clickNextStateEntry(state, s.id)">
|
||||
<i class="fa fa-check" ng-if="state.next_states_id.indexOf(s.id) > -1"></i>
|
||||
{{ s.name | translate }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -0,0 +1,29 @@
|
||||
<h1 ng-if="workflow" translate>Edit name</h1>
|
||||
<h1 ng-if="!workflow" translate>Create new workflow</h1>
|
||||
<div uib-alert ng-show="alert.show" class="alert-danger" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
|
||||
<form name="workflowForm" ng-submit="save()">
|
||||
<label for="name_1">
|
||||
<span ng-if="workflow" translate>
|
||||
Please enter a new workflow name:
|
||||
</span>
|
||||
<span ng-if="!workflow" translate>
|
||||
Please enter a name for the new workflow:
|
||||
</span>
|
||||
</label>
|
||||
<div>
|
||||
<div>
|
||||
<input class="form-control" id="name_1" type="text" ng-model="newName">
|
||||
</div>
|
||||
<div class="spacer-top-lg">
|
||||
<button type="submit" ng-disabled="newName == ''" class="btn btn-primary" translate>
|
||||
Save
|
||||
</button>
|
||||
<button ng-click="closeThisDialog()" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,46 @@
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="submenu">
|
||||
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-angle-double-left fa-lg"></i>
|
||||
<translate>Back to overview</translate>
|
||||
</a>
|
||||
<a href="" os-perms="motions.can_manage" class="btn btn-primary btn-sm" ng-click="create()">
|
||||
<i class="fa fa-plus fa-lg"></i>
|
||||
<translate>New</translate>
|
||||
</a>
|
||||
</div>
|
||||
<h1 translate>Workflows</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<div uib-alert ng-show="alert.show" class="alert-danger" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<translate>Name</translate>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="workflow in workflows | orderBy: 'name'">
|
||||
<td ng-mouseover="workflow.hover=true" ng-mouseleave="workflow.hover=false">
|
||||
<strong>{{ workflow.name | translate }}</strong>
|
||||
<div class="hoverActions" ng-class="{'hiddenDiv': !workflow.hover}">
|
||||
<!-- edit -->
|
||||
<a ui-sref="motions.workflow.detail({id: workflow.id})" translate>Edit</a> ·
|
||||
<!-- delete -->
|
||||
<a href="" class="text-danger"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
|
||||
<b>{{ workflow.name }}</b>"
|
||||
ng-bootbox-confirm-action="delete(workflow)" translate>Delete</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -5,6 +5,7 @@ from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models.deletion import ProtectedError
|
||||
from django.http import Http404
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.translation import ugettext as _
|
||||
@ -17,6 +18,7 @@ from ..utils.autoupdate import inform_changed_data
|
||||
from ..utils.collection import CollectionElement
|
||||
from ..utils.exceptions import OpenSlidesError
|
||||
from ..utils.rest_api import (
|
||||
CreateModelMixin,
|
||||
DestroyModelMixin,
|
||||
GenericViewSet,
|
||||
ModelViewSet,
|
||||
@ -45,7 +47,7 @@ from .models import (
|
||||
Submitter,
|
||||
Workflow,
|
||||
)
|
||||
from .serializers import MotionPollSerializer
|
||||
from .serializers import MotionPollSerializer, StateSerializer
|
||||
|
||||
|
||||
# Viewsets for the REST API
|
||||
@ -868,7 +870,23 @@ class MotionBlockViewSet(ModelViewSet):
|
||||
return Response({'detail': _('Followed recommendations successfully.')})
|
||||
|
||||
|
||||
class WorkflowViewSet(ModelViewSet):
|
||||
class ProtectedErrorMessageMixin:
|
||||
def getProtectedErrorMessage(self, name, error):
|
||||
# The protected objects can just be motions..
|
||||
motions = ['"' + str(m) + '"' for m in error.protected_objects.all()]
|
||||
count = len(motions)
|
||||
motions_verbose = ', '.join(motions[:3])
|
||||
if count > 3:
|
||||
motions_verbose += ', ...'
|
||||
|
||||
if count == 1:
|
||||
msg = _('This {} is assigned to motion {}.').format(name, motions_verbose)
|
||||
else:
|
||||
msg = _('This {} is assigned to motions {}.').format(name, motions_verbose)
|
||||
return msg + ' ' + _('Please remove all assignments before deletion.')
|
||||
|
||||
|
||||
class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin):
|
||||
"""
|
||||
API endpoint for workflows.
|
||||
|
||||
@ -893,6 +911,56 @@ class WorkflowViewSet(ModelViewSet):
|
||||
result = False
|
||||
return result
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
try:
|
||||
response = super().create(*args, **kwargs)
|
||||
except WorkflowError as e:
|
||||
raise ValidationError({'detail': e.args[0]})
|
||||
return response
|
||||
|
||||
def destroy(self, *args, **kwargs):
|
||||
"""
|
||||
Customized view endpoint to delete a motion poll.
|
||||
"""
|
||||
try:
|
||||
result = super().destroy(*args, **kwargs)
|
||||
except ProtectedError as e:
|
||||
msg = self.getProtectedErrorMessage('workflow', e)
|
||||
raise ValidationError({'detail': msg})
|
||||
return result
|
||||
|
||||
|
||||
class StateViewSet(CreateModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet,
|
||||
ProtectedErrorMessageMixin):
|
||||
"""
|
||||
API endpoint for workflow states.
|
||||
|
||||
There are the following views: create, update, partial_update and destroy.
|
||||
"""
|
||||
queryset = State.objects.all()
|
||||
serializer_class = StateSerializer
|
||||
|
||||
def check_view_permissions(self):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
return (has_perm(self.request.user, 'motions.can_see') and
|
||||
has_perm(self.request.user, 'motions.can_manage'))
|
||||
|
||||
def destroy(self, *args, **kwargs):
|
||||
"""
|
||||
Customized view endpoint to delete a motion poll.
|
||||
"""
|
||||
state = self.get_object()
|
||||
if state.workflow.first_state.pk == state.pk: # is this the first state of the workflow?
|
||||
raise ValidationError({'detail': _('You cannot delete the first state of the workflow.')})
|
||||
try:
|
||||
result = super().destroy(*args, **kwargs)
|
||||
except ProtectedError as e:
|
||||
msg = self.getProtectedErrorMessage('workflow', e)
|
||||
raise ValidationError({'detail': msg})
|
||||
return result
|
||||
|
||||
|
||||
# Special views
|
||||
|
||||
|
@ -16,36 +16,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* group list */
|
||||
#groups-table {
|
||||
table-layout: fixed;
|
||||
text-align: center;
|
||||
|
||||
thead tr th {
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.perm-head {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
tbody tr:first-child {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
tbody tr td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.optional-show { /* hide optional-show column */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,10 @@
|
||||
<p translate>
|
||||
All your changes are saved immediately. Changes you make are only effective once you (or the users concerned) reload the page.
|
||||
</p>
|
||||
<table id="groups-table" class="table table-bordered">
|
||||
<table id="multi-table" class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="perm-head">
|
||||
<th class="info-head">
|
||||
<h4 translate>Permissions</h4>
|
||||
<th ng-repeat="group in groups" ng-mouseover="group.hover=true" ng-mouseleave="group.hover=false">
|
||||
<span class="optional">
|
||||
@ -49,7 +49,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<tbody ng-repeat="app in apps" os-perms="users.can_manage">
|
||||
<tr class="pointer" ng-click="app.app_visible=!app.app_visible">
|
||||
<tr class="pointer bg-grey" ng-click="app.app_visible=!app.app_visible">
|
||||
<td>
|
||||
<b>{{ app.app_name | translate}}</b>
|
||||
<i class="fa" ng-class="app.app_visible ? 'fa-minus-square' : 'fa-plus-square'">
|
||||
|
@ -7,7 +7,11 @@ from rest_framework.decorators import detail_route, list_route # noqa
|
||||
from rest_framework.metadata import SimpleMetadata # noqa
|
||||
from rest_framework.mixins import ListModelMixin as _ListModelMixin
|
||||
from rest_framework.mixins import RetrieveModelMixin as _RetrieveModelMixin
|
||||
from rest_framework.mixins import DestroyModelMixin, UpdateModelMixin # noqa
|
||||
from rest_framework.mixins import ( # noqa
|
||||
CreateModelMixin,
|
||||
DestroyModelMixin,
|
||||
UpdateModelMixin,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.serializers import ModelSerializer as _ModelSerializer
|
||||
|
@ -1237,3 +1237,117 @@ class FollowRecommendationsForMotionBlock(TestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.id, self.state_id_accepted)
|
||||
self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).state.id, self.state_id_rejected)
|
||||
|
||||
|
||||
class CreateWorkflow(TestCase):
|
||||
"""
|
||||
Tests the creating of workflows.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.client.login(username='admin', password='admin')
|
||||
|
||||
def test_creation(self):
|
||||
Workflow.objects.all().delete()
|
||||
response = self.client.post(
|
||||
reverse('workflow-list'),
|
||||
{'name': 'test_name_OoCoo3MeiT9li5Iengu9'})
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
workflow = Workflow.objects.get()
|
||||
self.assertEqual(workflow.name, 'test_name_OoCoo3MeiT9li5Iengu9')
|
||||
first_state = workflow.first_state
|
||||
self.assertEqual(type(first_state), State)
|
||||
|
||||
def test_creation_with_wrong_first_state(self):
|
||||
response = self.client.post(
|
||||
reverse('workflow-list'),
|
||||
{'name': 'test_name_OoCoo3MeiT9li5Iengu9',
|
||||
'first_state': 1})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_creation_with_not_existing_first_state(self):
|
||||
Workflow.objects.all().delete()
|
||||
response = self.client.post(
|
||||
reverse('workflow-list'),
|
||||
{'name': 'test_name_OoCoo3MeiT9li5Iengu9',
|
||||
'first_state': 49})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class UpdateWorkflow(TestCase):
|
||||
"""
|
||||
Tests the updating of workflows.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.client.login(username='admin', password='admin')
|
||||
self.workflow = Workflow.objects.first()
|
||||
|
||||
def test_rename_workflow(self):
|
||||
response = self.client.patch(
|
||||
reverse('workflow-detail', args=[self.workflow.pk]),
|
||||
{'name': 'test_name_wofi38DiWLT"8d3lwfo3'})
|
||||
|
||||
workflow = Workflow.objects.get(pk=self.workflow.id)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(workflow.name, 'test_name_wofi38DiWLT"8d3lwfo3')
|
||||
|
||||
def test_change_first_state_correct(self):
|
||||
first_state = self.workflow.first_state
|
||||
other_workflow_state = self.workflow.states.exclude(pk=first_state.pk).first()
|
||||
response = self.client.patch(
|
||||
reverse('workflow-detail', args=[self.workflow.pk]),
|
||||
{'first_state': other_workflow_state.pk})
|
||||
|
||||
workflow = Workflow.objects.get(pk=self.workflow.id)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(workflow.first_state, other_workflow_state)
|
||||
|
||||
def test_change_first_state_not_existing(self):
|
||||
first_state = self.workflow.first_state
|
||||
response = self.client.patch(
|
||||
reverse('workflow-detail', args=[self.workflow.pk]),
|
||||
{'first_state': 42})
|
||||
|
||||
workflow = Workflow.objects.get(pk=self.workflow.id)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(workflow.first_state, first_state)
|
||||
|
||||
def test_change_first_state_wrong_workflow(self):
|
||||
first_state = self.workflow.first_state
|
||||
other_workflow = Workflow.objects.exclude(pk=self.workflow.pk).first()
|
||||
response = self.client.patch(
|
||||
reverse('workflow-detail', args=[self.workflow.pk]),
|
||||
{'first_state': other_workflow.first_state.pk})
|
||||
|
||||
workflow = Workflow.objects.get(pk=self.workflow.id)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(workflow.first_state, first_state)
|
||||
|
||||
|
||||
class DeleteWorkflow(TestCase):
|
||||
"""
|
||||
Tests the deletion of workflows.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.client.login(username='admin', password='admin')
|
||||
self.workflow = Workflow.objects.first()
|
||||
|
||||
def test_simple_delete(self):
|
||||
response = self.client.delete(
|
||||
reverse('workflow-detail', args=[self.workflow.pk]))
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(Workflow.objects.count(), 1) # Just the other default one
|
||||
|
||||
def test_delete_with_assigned_motions(self):
|
||||
self.motion = Motion(
|
||||
title='test_title_chee7ahCha6bingaew4e',
|
||||
text='test_text_birah1theL9ooseeFaip')
|
||||
self.motion.reset_state(self.workflow)
|
||||
self.motion.save()
|
||||
|
||||
response = self.client.delete(
|
||||
reverse('workflow-detail', args=[self.workflow.pk]))
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(Workflow.objects.count(), 2)
|
||||
|
Loading…
Reference in New Issue
Block a user