Added view to follow recommendations.
for all motions of a motion block
This commit is contained in:
parent
0270c31b32
commit
20f8875dcd
@ -177,12 +177,12 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
|
|
||||||
// Split up state name
|
// Split up state name
|
||||||
// example: "motions.motion.detail.update" -> ['motions', 'motion', 'detail', 'update']
|
// example: "motions.motion.detail.update" -> ['motions', 'motion', 'detail', 'update']
|
||||||
var patterns = state.name.split('.')
|
var patterns = state.name.split('.');
|
||||||
|
|
||||||
// set app and module name from state
|
// set app and module name from state
|
||||||
// - appName: patterns[0] (e.g. "motions")
|
// - appName: patterns[0] (e.g. "motions")
|
||||||
// - moduleNames: patterns without first element (e.g. ["motion", "detail", "update"])
|
// - moduleNames: patterns without first element (e.g. ["motion", "detail", "update"])
|
||||||
var appName = ''
|
var appName = '';
|
||||||
var moduleName = '';
|
var moduleName = '';
|
||||||
var moduleNames = [];
|
var moduleNames = [];
|
||||||
if (patterns.length > 0) {
|
if (patterns.length > 0) {
|
||||||
@ -194,7 +194,7 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
// example: ["motionBlock", "detail"] -> ["motion-block", "detail"]
|
// example: ["motionBlock", "detail"] -> ["motion-block", "detail"]
|
||||||
for (var i = 0; i < moduleNames.length; i++) {
|
for (var i = 0; i < moduleNames.length; i++) {
|
||||||
moduleNames[i] = moduleNames[i].replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase();
|
moduleNames[i] = moduleNames[i].replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase();
|
||||||
};
|
}
|
||||||
|
|
||||||
// use special templateUrl for create and update view
|
// use special templateUrl for create and update view
|
||||||
// example: ["motion", "detail", "update"] -> "motion-form"
|
// example: ["motion", "detail", "update"] -> "motion-form"
|
||||||
@ -1436,7 +1436,11 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
.filter("toArray", function(){
|
.filter('toArray', function(){
|
||||||
|
/*
|
||||||
|
* Transforms an object to an array. Items of the array are the values of
|
||||||
|
* the object elements.
|
||||||
|
*/
|
||||||
return function(obj) {
|
return function(obj) {
|
||||||
var result = [];
|
var result = [];
|
||||||
angular.forEach(obj, function(val, key) {
|
angular.forEach(obj, function(val, key) {
|
||||||
|
@ -17,6 +17,7 @@ from openslides.poll.models import (
|
|||||||
BaseVote,
|
BaseVote,
|
||||||
CollectDefaultVotesMixin,
|
CollectDefaultVotesMixin,
|
||||||
)
|
)
|
||||||
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.search import user_name_helper
|
from openslides.utils.search import user_name_helper
|
||||||
|
|
||||||
@ -192,7 +193,7 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
# TODO: Use transaction
|
# TODO: Use transaction
|
||||||
def save(self, use_version=None, *args, **kwargs):
|
def save(self, use_version=None, skip_autoupdate=False, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Save the motion.
|
Save the motion.
|
||||||
|
|
||||||
@ -225,14 +226,19 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
if not self.identifier and isinstance(self.identifier, str):
|
if not self.identifier and isinstance(self.identifier, str):
|
||||||
self.identifier = None
|
self.identifier = None
|
||||||
|
|
||||||
super(Motion, self).save(*args, **kwargs)
|
# Always skip autoupdate. Maybe we run it later in this method.
|
||||||
|
super(Motion, self).save(skip_autoupdate=True, *args, **kwargs)
|
||||||
|
|
||||||
if 'update_fields' in kwargs:
|
if 'update_fields' in kwargs:
|
||||||
# Do not save the version data if only some motion fields are updated.
|
# Do not save the version data if only some motion fields are updated.
|
||||||
|
if not skip_autoupdate:
|
||||||
|
inform_changed_data(self)
|
||||||
return
|
return
|
||||||
|
|
||||||
if use_version is False:
|
if use_version is False:
|
||||||
# We do not need to save the version.
|
# We do not need to save the version.
|
||||||
|
if not skip_autoupdate:
|
||||||
|
inform_changed_data(self)
|
||||||
return
|
return
|
||||||
elif use_version is None:
|
elif use_version is None:
|
||||||
use_version = self.get_last_version()
|
use_version = self.get_last_version()
|
||||||
@ -249,6 +255,8 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
if use_version.id is None:
|
if use_version.id is None:
|
||||||
if not self.version_data_changed(use_version):
|
if not self.version_data_changed(use_version):
|
||||||
# We do not need to save the version.
|
# We do not need to save the version.
|
||||||
|
if not skip_autoupdate:
|
||||||
|
inform_changed_data(self)
|
||||||
return
|
return
|
||||||
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
|
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
|
||||||
use_version.version_number = version_number + 1
|
use_version.version_number = version_number + 1
|
||||||
@ -256,16 +264,22 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
# Necessary line if the version was set before the motion got an id.
|
# Necessary line if the version was set before the motion got an id.
|
||||||
use_version.motion = use_version.motion
|
use_version.motion = use_version.motion
|
||||||
|
|
||||||
use_version.save()
|
# Always skip autoupdate. Maybe we run it later in this method.
|
||||||
|
use_version.save(skip_autoupdate=True)
|
||||||
|
|
||||||
# Set the active version of this motion. This has to be done after the
|
# Set the active version of this motion. This has to be done after the
|
||||||
# version is saved in the database.
|
# version is saved in the database.
|
||||||
# TODO: Move parts of these last lines of code outside the save method
|
# TODO: Move parts of these last lines of code outside the save method
|
||||||
# when other versions than the last ones should be edited later on.
|
# when other versions than the last one should be edited later on.
|
||||||
if self.active_version is None or not self.state.leave_old_version_active:
|
if self.active_version is None or not self.state.leave_old_version_active:
|
||||||
# TODO: Don't call this if it was not a new version
|
# TODO: Don't call this if it was not a new version
|
||||||
self.active_version = use_version
|
self.active_version = use_version
|
||||||
self.save(update_fields=['active_version'])
|
# Always skip autoupdate. Maybe we run it later in this method.
|
||||||
|
self.save(update_fields=['active_version'], skip_autoupdate=True)
|
||||||
|
|
||||||
|
# Finally run autoupdate if it is not skipped by caller.
|
||||||
|
if not skip_autoupdate:
|
||||||
|
inform_changed_data(self)
|
||||||
|
|
||||||
def version_data_changed(self, version):
|
def version_data_changed(self, version):
|
||||||
"""
|
"""
|
||||||
@ -530,6 +544,13 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
recommendation = State.objects.get(pk=recommendation)
|
recommendation = State.objects.get(pk=recommendation)
|
||||||
self.recommendation = recommendation
|
self.recommendation = recommendation
|
||||||
|
|
||||||
|
def follow_recommendation(self):
|
||||||
|
"""
|
||||||
|
Set the state of this motion to its recommendation.
|
||||||
|
"""
|
||||||
|
if self.recommendation is not None:
|
||||||
|
self.set_state(self.recommendation)
|
||||||
|
|
||||||
def get_agenda_title(self):
|
def get_agenda_title(self):
|
||||||
"""
|
"""
|
||||||
Return a simple title string for the agenda.
|
Return a simple title string for the agenda.
|
||||||
@ -624,7 +645,7 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
|
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def write_log(self, message_list, person=None):
|
def write_log(self, message_list, person=None, skip_autoupdate=False):
|
||||||
"""
|
"""
|
||||||
Write a log message.
|
Write a log message.
|
||||||
|
|
||||||
@ -633,7 +654,8 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
if person and not person.is_authenticated():
|
if person and not person.is_authenticated():
|
||||||
person = None
|
person = None
|
||||||
MotionLog.objects.create(motion=self, message_list=message_list, person=person)
|
motion_log = MotionLog(motion=self, message_list=message_list, person=person)
|
||||||
|
motion_log.save(skip_autoupdate=skip_autoupdate)
|
||||||
|
|
||||||
def is_amendment(self):
|
def is_amendment(self):
|
||||||
"""
|
"""
|
||||||
|
@ -127,6 +127,7 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
|
|||||||
|
|
||||||
.controller('MotionBlockDetailCtrl', [
|
.controller('MotionBlockDetailCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
|
'$http',
|
||||||
'ngDialog',
|
'ngDialog',
|
||||||
'Motion',
|
'Motion',
|
||||||
'MotionBlockForm',
|
'MotionBlockForm',
|
||||||
@ -134,7 +135,7 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
|
|||||||
'motionBlock',
|
'motionBlock',
|
||||||
'Projector',
|
'Projector',
|
||||||
'ProjectionDefault',
|
'ProjectionDefault',
|
||||||
function($scope, ngDialog, Motion, MotionBlockForm, MotionBlock, motionBlock, Projector, ProjectionDefault) {
|
function($scope, $http, ngDialog, Motion, MotionBlockForm, MotionBlock, motionBlock, Projector, ProjectionDefault) {
|
||||||
MotionBlock.bindOne(motionBlock.id, $scope, 'motionBlock');
|
MotionBlock.bindOne(motionBlock.id, $scope, 'motionBlock');
|
||||||
Motion.bindAll({}, $scope, 'motions');
|
Motion.bindAll({}, $scope, 'motions');
|
||||||
$scope.$watch(function () {
|
$scope.$watch(function () {
|
||||||
@ -148,6 +149,15 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
|
|||||||
$scope.openDialog = function (topic) {
|
$scope.openDialog = function (topic) {
|
||||||
ngDialog.open(MotionBlockForm.getDialog(motionBlock));
|
ngDialog.open(MotionBlockForm.getDialog(motionBlock));
|
||||||
};
|
};
|
||||||
|
$scope.followRecommendations = function () {
|
||||||
|
$http.post('/rest/motions/motion-block/' + motionBlock.id + '/follow_recommendations/')
|
||||||
|
.success(function(data) {
|
||||||
|
$scope.alert = { type: 'success', msg: data.detail, show: true };
|
||||||
|
})
|
||||||
|
.error(function(data) {
|
||||||
|
$scope.alert = { type: 'danger', msg: data.detail, show: true };
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -26,12 +26,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<!-- set state button (TODO)-->
|
|
||||||
<a os-perms="motions.can_manage" class="btn btn-default btn"
|
<a os-perms="motions.can_manage" class="btn btn-default btn"
|
||||||
ng-bootbox-confirm="{{ 'Are you sure you want to override the state of all motions of this motion block?' | translate }}"
|
ng-bootbox-confirm="{{ 'Are you sure you want to override the state of all motions of this motion block?' | translate }}"
|
||||||
ng-bootbox-confirm-action="" translate>
|
ng-bootbox-confirm-action="followRecommendations()" translate>
|
||||||
<i class="fa fa-magic fa-lg"></i>
|
<i class="fa fa-magic fa-lg"></i>
|
||||||
<translate>Set state for each motion according to their recommendation</translate>
|
<translate>Follow recommendations for all motions</translate>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="row spacer form-group">
|
<div class="row spacer form-group">
|
||||||
|
@ -9,8 +9,9 @@ from django.utils.translation import ugettext_noop
|
|||||||
from reportlab.platypus import SimpleDocTemplate
|
from reportlab.platypus import SimpleDocTemplate
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from openslides.core.config import config
|
from ..core.config import config
|
||||||
from openslides.utils.rest_api import (
|
from ..utils.autoupdate import inform_changed_data
|
||||||
|
from ..utils.rest_api import (
|
||||||
DestroyModelMixin,
|
DestroyModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
@ -19,8 +20,7 @@ from openslides.utils.rest_api import (
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
)
|
)
|
||||||
from openslides.utils.views import APIView, PDFView, SingleObjectMixin
|
from ..utils.views import APIView, PDFView, SingleObjectMixin
|
||||||
|
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
@ -467,13 +467,35 @@ class MotionBlockViewSet(ModelViewSet):
|
|||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action == 'metadata':
|
elif self.action == 'metadata':
|
||||||
result = self.request.user.has_perm('motions.can_see')
|
result = self.request.user.has_perm('motions.can_see')
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
elif self.action in ('create', 'partial_update', 'update', 'destroy', 'follow_recommendations'):
|
||||||
result = (self.request.user.has_perm('motions.can_see') and
|
result = (self.request.user.has_perm('motions.can_see') and
|
||||||
self.request.user.has_perm('motions.can_manage'))
|
self.request.user.has_perm('motions.can_manage'))
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@detail_route(methods=['post'])
|
||||||
|
def follow_recommendations(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
View to set the states of all motions of this motion block each to
|
||||||
|
its recommendation. It is a POST request without any data.
|
||||||
|
"""
|
||||||
|
motion_block = self.get_object()
|
||||||
|
instances = []
|
||||||
|
with transaction.atomic():
|
||||||
|
for motion in motion_block.motion_set.all():
|
||||||
|
# Follow recommendation.
|
||||||
|
motion.follow_recommendation()
|
||||||
|
motion.save(skip_autoupdate=True)
|
||||||
|
# Write the log message.
|
||||||
|
motion.write_log(
|
||||||
|
message_list=[ugettext_noop('State set to'), ' ', motion.state.name],
|
||||||
|
person=request.user,
|
||||||
|
skip_autoupdate=True)
|
||||||
|
instances.append(motion)
|
||||||
|
inform_changed_data(instances)
|
||||||
|
return Response({'detail': _('Followed recommendations successfully.')})
|
||||||
|
|
||||||
|
|
||||||
class WorkflowViewSet(ModelViewSet):
|
class WorkflowViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -145,7 +145,7 @@ def inform_changed_data(instances, information=None):
|
|||||||
"""
|
"""
|
||||||
root_instances = set()
|
root_instances = set()
|
||||||
if not isinstance(instances, Iterable):
|
if not isinstance(instances, Iterable):
|
||||||
# Make surce instance is an iterable
|
# Make sure instances is an iterable
|
||||||
instances = (instances, )
|
instances = (instances, )
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
try:
|
try:
|
||||||
|
@ -7,7 +7,7 @@ from rest_framework.test import APIClient
|
|||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
from openslides.motions.models import Category, Motion, State
|
from openslides.motions.models import Category, Motion, MotionBlock, State
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
@ -726,3 +726,40 @@ class NumberMotionsInCategory(TestCase):
|
|||||||
self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, None)
|
self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, None)
|
||||||
self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2')
|
self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2')
|
||||||
self.assertEqual(Motion.objects.get(pk=self.motion_3.pk).identifier, 'test_prefix_ahz6tho2mooH8 1')
|
self.assertEqual(Motion.objects.get(pk=self.motion_3.pk).identifier, 'test_prefix_ahz6tho2mooH8 1')
|
||||||
|
|
||||||
|
|
||||||
|
class FollowRecommendationsForMotionBlock(TestCase):
|
||||||
|
"""
|
||||||
|
Tests following the recommendations of motions in an motion block.
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
self.state_id_accepted = 2 # This should be the id of the state 'accepted'.
|
||||||
|
self.state_id_rejected = 3 # This should be the id of the state 'rejected'.
|
||||||
|
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.login(username='admin', password='admin')
|
||||||
|
|
||||||
|
self.motion_block = MotionBlock.objects.create(
|
||||||
|
title='test_motion_block_name_Ufoopiub7quaezaepeic')
|
||||||
|
|
||||||
|
self.motion = Motion(
|
||||||
|
title='test_title_yo8ohy5eifeiyied2AeD',
|
||||||
|
text='test_text_chi1aeth5faPhueQu8oh',
|
||||||
|
motion_block=self.motion_block)
|
||||||
|
self.motion.save()
|
||||||
|
self.motion.set_recommendation(self.state_id_accepted)
|
||||||
|
self.motion.save()
|
||||||
|
|
||||||
|
self.motion_2 = Motion(
|
||||||
|
title='test_title_eith0EemaW8ahZa9Piej',
|
||||||
|
text='test_text_haeho1ohk3ou7pau2Jee',
|
||||||
|
motion_block=self.motion_block)
|
||||||
|
self.motion_2.save()
|
||||||
|
self.motion_2.set_recommendation(self.state_id_rejected)
|
||||||
|
self.motion_2.save()
|
||||||
|
|
||||||
|
def test_follow_recommendations_for_motion_block(self):
|
||||||
|
response = self.client.post(reverse('motionblock-follow-recommendations', args=[self.motion_block.pk]))
|
||||||
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user