Merge pull request #2782 from FinnStutzenstein/AssignmentSort
Sort candidates in assignments
This commit is contained in:
commit
16ff9200a9
@ -3,6 +3,7 @@ from django.db import transaction
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openslides.core.config import config
|
||||
from openslides.utils.autoupdate import inform_changed_data
|
||||
from openslides.utils.exceptions import OpenSlidesError
|
||||
from openslides.utils.rest_api import (
|
||||
GenericViewSet,
|
||||
@ -211,9 +212,12 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
with transaction.atomic():
|
||||
for speaker in valid_speakers:
|
||||
speaker.weight = weight
|
||||
speaker.save()
|
||||
speaker.save(skip_autoupdate=True)
|
||||
weight += 1
|
||||
|
||||
# send autoupdate
|
||||
inform_changed_data(item)
|
||||
|
||||
# Initiate response.
|
||||
return Response({'detail': _('List of speakers successfully sorted.')})
|
||||
|
||||
|
25
openslides/assignments/migrations/0003_candidate_weight.py
Normal file
25
openslides/assignments/migrations/0003_candidate_weight.py
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2016-12-16 11:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assignments', '0002_assignmentpoll_pollmethod'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assignmentrelateduser',
|
||||
name='weight',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assignmentoption',
|
||||
name='weight',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
@ -28,14 +28,31 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Many to Many table between an assignment and user.
|
||||
"""
|
||||
|
||||
assignment = models.ForeignKey(
|
||||
'Assignment',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignment_related_users')
|
||||
"""
|
||||
ForeinKey to the assignment.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)
|
||||
"""
|
||||
ForeinKey to the user who is related to the assignment.
|
||||
"""
|
||||
|
||||
elected = models.BooleanField(default=False)
|
||||
"""
|
||||
Saves the election state of each user
|
||||
"""
|
||||
|
||||
weight = models.IntegerField(default=0)
|
||||
"""
|
||||
The sort order of the candidates.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
@ -193,9 +210,14 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Adds the user as candidate.
|
||||
"""
|
||||
weight = self.assignment_related_users.aggregate(
|
||||
models.Max('weight'))['weight__max'] or 0
|
||||
defaults = {
|
||||
'elected': False,
|
||||
'weight': weight + 1}
|
||||
related_user, __ = self.assignment_related_users.update_or_create(
|
||||
user=user,
|
||||
defaults={'elected': False})
|
||||
defaults=defaults)
|
||||
|
||||
def set_elected(self, user):
|
||||
"""
|
||||
@ -249,7 +271,13 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
poll = self.polls.create(
|
||||
description=self.poll_description_default,
|
||||
pollmethod=pollmethod)
|
||||
poll.set_options({'candidate': user} for user in candidates)
|
||||
options = []
|
||||
related_users = AssignmentRelatedUser.objects.filter(assignment__id=self.id)
|
||||
for related_user in related_users:
|
||||
options.append({
|
||||
'candidate': related_user.user,
|
||||
'weight': related_user.weight})
|
||||
poll.set_options(options)
|
||||
|
||||
# Add all candidates to list of speakers of related agenda item
|
||||
# TODO: Try to do this in a bulk create
|
||||
@ -360,6 +388,8 @@ class AssignmentOption(RESTModelMixin, BaseOption):
|
||||
candidate = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)
|
||||
weight = models.IntegerField(default=0)
|
||||
|
||||
vote_class = AssignmentVote
|
||||
|
||||
class Meta:
|
||||
|
@ -32,7 +32,8 @@ class AssignmentRelatedUserSerializer(ModelSerializer):
|
||||
'id',
|
||||
'user',
|
||||
'elected',
|
||||
'assignment') # js-data needs the assignment-id in the nested object to define relations.
|
||||
'assignment',
|
||||
'weight') # js-data needs the assignment-id in the nested object to define relations.
|
||||
|
||||
|
||||
class AssignmentVoteSerializer(ModelSerializer):
|
||||
@ -53,7 +54,7 @@ class AssignmentOptionSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AssignmentOption
|
||||
fields = ('id', 'candidate', 'is_elected', 'votes', 'poll')
|
||||
fields = ('id', 'candidate', 'is_elected', 'votes', 'poll', 'weight')
|
||||
|
||||
def get_is_elected(self, obj):
|
||||
"""
|
||||
|
@ -5,9 +5,10 @@
|
||||
angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
|
||||
.factory('AssignmentContentProvider', [
|
||||
'$filter',
|
||||
'gettextCatalog',
|
||||
'PDFLayout',
|
||||
function(gettextCatalog, PDFLayout) {
|
||||
function($filter, gettextCatalog, PDFLayout) {
|
||||
|
||||
var createInstance = function(assignment) {
|
||||
|
||||
@ -60,10 +61,11 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
// show candidate list (if assignment phase is not 'finished')
|
||||
var createCandidateList = function() {
|
||||
if (assignment.phase != 2) {
|
||||
var candidates = $filter('orderBy')(assignment.assignment_related_users, 'weight');
|
||||
var candidatesText = gettextCatalog.getString("Candidates") + ": ";
|
||||
var userList = [];
|
||||
|
||||
angular.forEach(assignment.assignment_related_users, function(assignmentsRelatedUser) {
|
||||
angular.forEach(candidates, function(assignmentsRelatedUser) {
|
||||
userList.push({
|
||||
text: assignmentsRelatedUser.user.get_full_name(),
|
||||
margin: [0, 0, 0, 10],
|
||||
@ -263,9 +265,10 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
}])
|
||||
|
||||
.factory('BallotContentProvider', [
|
||||
'$filter',
|
||||
'gettextCatalog',
|
||||
'PDFLayout',
|
||||
function(gettextCatalog, PDFLayout) {
|
||||
function($filter, gettextCatalog, PDFLayout) {
|
||||
|
||||
var createInstance = function(scope, poll, pollNumber) {
|
||||
|
||||
@ -324,15 +327,16 @@ angular.module('OpenSlidesApp.assignments.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
};
|
||||
|
||||
var createSelectionField = function() {
|
||||
var candidates = $filter('orderBy')(poll.options, 'weight');
|
||||
var candidateBallotList = [];
|
||||
|
||||
if (poll.pollmethod == 'votes') {
|
||||
angular.forEach(poll.options, function(option) {
|
||||
angular.forEach(candidates, function(option) {
|
||||
var candidate = option.candidate.get_full_name();
|
||||
candidateBallotList.push(PDFLayout.createBallotEntry(candidate));
|
||||
});
|
||||
} else {
|
||||
angular.forEach(poll.options, function(option) {
|
||||
angular.forEach(candidates, function(option) {
|
||||
var candidate = option.candidate.get_full_name();
|
||||
candidateBallotList.push(createYNBallotEntry(candidate));
|
||||
});
|
||||
|
@ -440,6 +440,7 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
.controller('AssignmentDetailCtrl', [
|
||||
'$scope',
|
||||
'$http',
|
||||
'$filter',
|
||||
'filterFilter',
|
||||
'gettext',
|
||||
'ngDialog',
|
||||
@ -456,11 +457,10 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
'PdfMakeDocumentProvider',
|
||||
'PdfMakeBallotPaperProvider',
|
||||
'gettextCatalog',
|
||||
function($scope, $http, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment, User,
|
||||
assignment, phases, Projector, ProjectionDefault, AssignmentContentProvider, BallotContentProvider,
|
||||
function($scope, $http, $filter, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment,
|
||||
User, assignment, phases, Projector, ProjectionDefault, AssignmentContentProvider, BallotContentProvider,
|
||||
PdfMakeDocumentProvider, PdfMakeBallotPaperProvider, gettextCatalog) {
|
||||
User.bindAll({}, $scope, 'users');
|
||||
Assignment.bindOne(assignment.id, $scope, 'assignment');
|
||||
Assignment.loadRelations(assignment, 'agenda_item');
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified();
|
||||
@ -470,6 +470,13 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
$scope.defaultProjectorId = projectiondefault.projector_id;
|
||||
}
|
||||
});
|
||||
$scope.$watch(function () {
|
||||
return Assignment.lastModified(assignment.id);
|
||||
}, function () {
|
||||
// setup sorting of candidates
|
||||
$scope.relatedUsersSorted = $filter('orderBy')(assignment.assignment_related_users, 'weight');
|
||||
$scope.assignment = Assignment.get(assignment.id);
|
||||
});
|
||||
$scope.candidateSelectBox = {};
|
||||
$scope.phases = phases;
|
||||
$scope.alert = {};
|
||||
@ -532,6 +539,18 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
else
|
||||
return false;
|
||||
};
|
||||
// Sort all candidates
|
||||
$scope.treeOptions = {
|
||||
dropped: function () {
|
||||
var sortedCandidates = [];
|
||||
_.forEach($scope.relatedUsersSorted, function (user) {
|
||||
sortedCandidates.push(user.id);
|
||||
});
|
||||
$http.post('/rest/assignments/assignment/' + $scope.assignment.id + '/sort_related_users/',
|
||||
{related_users: sortedCandidates}
|
||||
);
|
||||
}
|
||||
};
|
||||
// update phase
|
||||
$scope.updatePhase = function (phase_id) {
|
||||
assignment.phase = phase_id;
|
||||
@ -730,11 +749,12 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
|
||||
.controller('AssignmentPollUpdateCtrl', [
|
||||
'$scope',
|
||||
'$filter',
|
||||
'gettextCatalog',
|
||||
'AssignmentPoll',
|
||||
'assignmentpoll',
|
||||
'ballot',
|
||||
function($scope, gettextCatalog, AssignmentPoll, assignmentpoll, ballot) {
|
||||
function($scope, $filter, gettextCatalog, AssignmentPoll, assignmentpoll, ballot) {
|
||||
// set initial values for form model by create deep copy of assignmentpoll object
|
||||
// so detail view is not updated while editing poll
|
||||
$scope.model = angular.copy(assignmentpoll);
|
||||
@ -743,7 +763,8 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
$scope.alert = {};
|
||||
|
||||
// add dynamic form fields
|
||||
assignmentpoll.options.forEach(function(option) {
|
||||
var options = $filter('orderBy')(assignmentpoll.options, 'weight');
|
||||
options.forEach(function(option) {
|
||||
var defaultValue;
|
||||
if (assignmentpoll.pollmethod == 'yna' || assignmentpoll.pollmethod == 'yn') {
|
||||
if (assignmentpoll.pollmethod == 'yna') {
|
||||
|
@ -82,17 +82,21 @@
|
||||
|
||||
<div ng-if="assignment.phase != 2">
|
||||
<h3 translate>Candidates</h3>
|
||||
<ul>
|
||||
<li ng-repeat="related_user in assignment.assignment_related_users">
|
||||
<a ui-sref="users.user.detail({id: related_user.user_id})">{{ related_user.user.get_full_name() }}</a>
|
||||
<i ng-if="related_user.elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
|
||||
<button os-perms="assignments.can_manage" ng-click="removeCandidate(related_user.user_id)"
|
||||
class="btn btn-default btn-xs">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</ul>
|
||||
<div ui-tree="treeOptions" ng-if="assignment.assignment_related_users.length">
|
||||
<ol ui-tree-nodes="" ng-model="relatedUsersSorted">
|
||||
<li ui-tree-node ng-repeat="related_user in assignment.assignment_related_users | orderBy:'weight'">
|
||||
<i ui-tree-handle="" class="fa fa-arrows-v spacer-right" os-perms="assignments.can_manage"></i>
|
||||
<a class="spacer-right" ui-sref="users.user.detail({id: related_user.user_id})">
|
||||
{{ related_user.user.get_full_name() }}
|
||||
</a>
|
||||
<i ng-if="related_user.elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
|
||||
<a href os-perms="assignments.can_manage" ng-click="removeCandidate(related_user.user_id)" class="btn btn-default btn-xs">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group spacer-top-lg">
|
||||
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</div>
|
||||
@ -174,7 +178,7 @@
|
||||
<div ng-if="!poll.has_votes">
|
||||
<strong translate>Candidates</strong>
|
||||
<ul class="list-unstyled">
|
||||
<li ng-repeat="option in poll.options">
|
||||
<li ng-repeat="option in poll.options | orderBy:'weight'">
|
||||
<a ui-sref="users.user.detail({id: option.candidate.id})">
|
||||
{{ option.candidate.get_full_name() }}
|
||||
</a>
|
||||
@ -202,7 +206,7 @@
|
||||
<th translate ng-hide="method === 'disabled'">Quorum
|
||||
</th>
|
||||
<!-- candidates (poll options) -->
|
||||
<tr ng-repeat="option in poll.options">
|
||||
<tr ng-repeat="option in poll.options | orderBy:'weight'">
|
||||
<!-- candidate name -->
|
||||
<td>
|
||||
<span os-perms="assignments.can_manage">
|
||||
|
@ -17,7 +17,7 @@
|
||||
<div ng-if="!showResult">
|
||||
<h3 translate>Candidates</h3>
|
||||
<ul>
|
||||
<li ng-repeat="related_user in assignment.assignment_related_users">
|
||||
<li ng-repeat="related_user in assignment.assignment_related_users | orderBy:'weight'">
|
||||
{{ related_user.user.get_full_name() }}
|
||||
<i ng-if="related_user.elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
|
||||
</ul>
|
||||
@ -31,7 +31,7 @@
|
||||
<th ng-if="poll.has_votes" class="col-sm-6" translate>Votes</th>
|
||||
|
||||
<!-- candidates (poll options) -->
|
||||
<tr ng-repeat="option in poll.options">
|
||||
<tr ng-repeat="option in poll.options | orderBy:'weight'">
|
||||
|
||||
<!-- candidate name -->
|
||||
<td>
|
||||
|
@ -2,6 +2,7 @@ from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openslides.utils.autoupdate import inform_changed_data
|
||||
from openslides.utils.rest_api import (
|
||||
DestroyModelMixin,
|
||||
GenericViewSet,
|
||||
@ -13,7 +14,7 @@ from openslides.utils.rest_api import (
|
||||
)
|
||||
|
||||
from .access_permissions import AssignmentAccessPermissions
|
||||
from .models import Assignment, AssignmentPoll
|
||||
from .models import Assignment, AssignmentPoll, AssignmentRelatedUser
|
||||
from .serializers import AssignmentAllPollSerializer
|
||||
|
||||
|
||||
@ -40,7 +41,7 @@ class AssignmentViewSet(ModelViewSet):
|
||||
# Everybody is allowed to see the metadata.
|
||||
result = True
|
||||
elif self.action in ('create', 'partial_update', 'update', 'destroy',
|
||||
'mark_elected', 'create_poll'):
|
||||
'mark_elected', 'create_poll', 'sort_related_users'):
|
||||
result = (self.request.user.has_perm('assignments.can_see') and
|
||||
self.request.user.has_perm('assignments.can_manage'))
|
||||
elif self.action == 'candidature_self':
|
||||
@ -186,6 +187,48 @@ class AssignmentViewSet(ModelViewSet):
|
||||
assignment.create_poll()
|
||||
return Response({'detail': _('Ballot created successfully.')})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def sort_related_users(self, request, pk=None):
|
||||
"""
|
||||
Special view endpoint to sort the assignment related users.
|
||||
|
||||
Expects a list of IDs of the related users (pk of AssignmentRelatedUser model).
|
||||
"""
|
||||
assignment = self.get_object()
|
||||
|
||||
# Check data
|
||||
related_user_ids = request.data.get('related_users')
|
||||
if not isinstance(related_user_ids, list):
|
||||
raise ValidationError(
|
||||
{'detail': _('users has to be a list of IDs.')})
|
||||
|
||||
# Get all related users from AssignmentRelatedUser.
|
||||
related_users = {}
|
||||
for related_user in AssignmentRelatedUser.objects.filter(assignment__id=assignment.id):
|
||||
related_users[related_user.pk] = related_user
|
||||
|
||||
# Check all given candidates from the request
|
||||
valid_related_users = []
|
||||
for related_user_id in related_user_ids:
|
||||
if not isinstance(related_user_id, int) or related_users.get(related_user_id) is None:
|
||||
raise ValidationError(
|
||||
{'detail': _('Invalid data.')})
|
||||
valid_related_users.append(related_users[related_user_id])
|
||||
|
||||
# Sort the related users
|
||||
weight = 1
|
||||
with transaction.atomic():
|
||||
for valid_related_user in valid_related_users:
|
||||
valid_related_user.weight = weight
|
||||
valid_related_user.save(skip_autoupdate=True)
|
||||
weight += 1
|
||||
|
||||
# send autoupdate
|
||||
inform_changed_data(assignment)
|
||||
|
||||
# Initiate response.
|
||||
return Response({'detail': _('Assignment related users successfully sorted.')})
|
||||
|
||||
|
||||
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user