Merge pull request #2782 from FinnStutzenstein/AssignmentSort

Sort candidates in assignments
This commit is contained in:
Emanuel Schütze 2016-12-19 19:47:38 +01:00 committed by GitHub
commit 16ff9200a9
9 changed files with 163 additions and 31 deletions

View File

@ -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.')})

View 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),
),
]

View File

@ -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:

View File

@ -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):
"""

View File

@ -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));
});

View File

@ -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') {

View File

@ -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">

View File

@ -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>

View File

@ -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):
"""