Merge pull request #1815 from emanuelschuetze/candidateElected

Mark candidate as elected. Updated assignment detail.
This commit is contained in:
Oskar Hahn 2016-01-09 22:37:39 +01:00
commit c30116f5c1
7 changed files with 106 additions and 49 deletions

View File

@ -189,7 +189,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
function ($scope, $filter, $http, Agenda, User, item) { function ($scope, $filter, $http, Agenda, User, item) {
Agenda.bindOne(item.id, $scope, 'item'); Agenda.bindOne(item.id, $scope, 'item');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
$scope.speaker = {}; $scope.speakerSelectBox = {};
$scope.alert = {}; $scope.alert = {};
$scope.speakers = $filter('orderBy')(item.speakers, 'weight'); $scope.speakers = $filter('orderBy')(item.speakers, 'weight');
$scope.$watch(function () { $scope.$watch(function () {
@ -208,9 +208,11 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
.success(function(data){ .success(function(data){
$scope.alert.show = false; $scope.alert.show = false;
$scope.speakers = item.speakers; $scope.speakers = item.speakers;
$scope.speakerSelectBox = {};
}) })
.error(function(data){ .error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true }; $scope.alert = { type: 'danger', msg: data.detail, show: true };
$scope.speakerSelectBox = {};
}); });
}; };
// delete speaker(!) from list of speakers // delete speaker(!) from list of speakers

View File

@ -89,7 +89,7 @@
{{alert.msg}} {{alert.msg}}
</alert> </alert>
<div os-perms="agenda.can_manage" class="input-group"> <div os-perms="agenda.can_manage" class="input-group">
<ui-select ng-model="speaker.selected" ng-change="addSpeaker(speaker.selected.id)"> <ui-select ng-model="speakerSelectBox.selected" ng-change="addSpeaker(speakerSelectBox.selected.id)">
<ui-select-match placeholder="{{ 'Select or search a participant ...' | translate }}"> <ui-select-match placeholder="{{ 'Select or search a participant ...' | translate }}">
{{ $select.selected.get_full_name() }} {{ $select.selected.get_full_name() }}
</ui-select-match> </ui-select-match>
@ -98,7 +98,7 @@
</ui-select-choices> </ui-select-choices>
</ui-select> </ui-select>
<span class="input-group-btn"> <span class="input-group-btn">
<a ng-click="speaker={}" class="btn btn-default"> <a ng-click="speakerSelectBox={}" class="btn btn-default">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
</a> </a>
</span> </span>

View File

@ -7,6 +7,7 @@ from openslides.utils.rest_api import (
ListField, ListField,
ListSerializer, ListSerializer,
ModelSerializer, ModelSerializer,
SerializerMethodField,
ValidationError, ValidationError,
) )
@ -47,10 +48,17 @@ class AssignmentOptionSerializer(ModelSerializer):
Serializer for assignment.models.AssignmentOption objects. Serializer for assignment.models.AssignmentOption objects.
""" """
votes = AssignmentVoteSerializer(many=True, read_only=True) votes = AssignmentVoteSerializer(many=True, read_only=True)
is_elected = SerializerMethodField()
class Meta: class Meta:
model = AssignmentOption model = AssignmentOption
fields = ('id', 'candidate', 'votes', 'poll') fields = ('id', 'candidate', 'is_elected', 'votes', 'poll')
def get_is_elected(self, obj):
"""
Returns the election status of the candidate of this option.
"""
return obj.poll.assignment.is_elected(obj.candidate)
class FilterPollListSerializer(ListSerializer): class FilterPollListSerializer(ListSerializer):
@ -81,6 +89,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
child=IntegerField(min_value=-2)), child=IntegerField(min_value=-2)),
write_only=True, write_only=True,
required=False) required=False)
has_votes = SerializerMethodField()
class Meta: class Meta:
model = AssignmentPoll model = AssignmentPoll
@ -94,9 +103,16 @@ class AssignmentAllPollSerializer(ModelSerializer):
'votesinvalid', 'votesinvalid',
'votescast', 'votescast',
'votes', 'votes',
'has_votes',
'assignment') # js-data needs the assignment-id in the nested object to define relations. 'assignment') # js-data needs the assignment-id in the nested object to define relations.
read_only_fields = ('yesnoabstain',) read_only_fields = ('yesnoabstain',)
def get_has_votes(self, obj):
"""
Returns True if this poll has some votes.
"""
return obj.has_votes()
@transaction.atomic @transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
""" """
@ -142,7 +158,7 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
""" """
Serializer for assignment.models.AssignmentPoll objects. Serializer for assignment.models.AssignmentPoll objects.
Serializes only short polls. Serializes only short polls (excluded unpublished polls).
""" """
class Meta: class Meta:
list_serializer_class = FilterPollListSerializer list_serializer_class = FilterPollListSerializer

View File

@ -245,16 +245,18 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Assignment.bindOne(assignment.id, $scope, 'assignment'); Assignment.bindOne(assignment.id, $scope, 'assignment');
Assignment.loadRelations(assignment, 'agenda_item'); Assignment.loadRelations(assignment, 'agenda_item');
$scope.candidate = {}; $scope.candidateSelectBox = {};
$scope.alert = {}; $scope.alert = {};
// add (nominate) candidate // add (nominate) candidate
$scope.addCandidate = function (userId) { $scope.addCandidate = function (userId) {
$http.post('/rest/assignments/assignment/' + assignment.id + '/candidature_other/', {'user': userId}) $http.post('/rest/assignments/assignment/' + assignment.id + '/candidature_other/', {'user': userId})
.success(function(data){ .success(function(data){
$scope.alert.show = false; $scope.alert.show = false;
$scope.candidateSelectBox = {};
}) })
.error(function(data){ .error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true }; $scope.alert = { type: 'danger', msg: data.detail, show: true };
$scope.candidateSelectBox = {};
}); });
}; };
// remove candidate // remove candidate
@ -266,15 +268,6 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
$scope.alert = { type: 'danger', msg: data.detail, show: true }; $scope.alert = { type: 'danger', msg: data.detail, show: true };
}); });
}; };
// remove blocked candidate from "block-list"
$scope.removeBlockedCandidate = function (userId) {
$http.delete('/rest/assignments/assignment/' + assignment.id + '/candidature_other/',
{headers: {'Content-Type': 'application/json'},
data: JSON.stringify({user: userId})})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// add me (nominate self as candidate) // add me (nominate self as candidate)
$scope.addMe = function () { $scope.addMe = function () {
$http.post('/rest/assignments/assignment/' + assignment.id + '/candidature_self/', {}) $http.post('/rest/assignments/assignment/' + assignment.id + '/candidature_self/', {})
@ -316,7 +309,7 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
$scope.alert = { type: 'danger', msg: data.detail, show: true }; $scope.alert = { type: 'danger', msg: data.detail, show: true };
}); });
}; };
// delete ballt // delete ballot
$scope.deleteBallot = function (poll) { $scope.deleteBallot = function (poll) {
poll.DSDestroy(); poll.DSDestroy();
} }
@ -353,6 +346,18 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
$scope.alert = { type: 'danger', msg: message, show: true }; $scope.alert = { type: 'danger', msg: message, show: true };
}); });
}; };
// mark candidate as (not) elected
$scope.markElected = function (user, reverse) {
if (reverse) {
$http.delete(
'/rest/assignments/assignment/' + assignment.id + '/mark_elected/',
{headers: {'Content-Type': 'application/json'},
data: JSON.stringify({user: user})})
} else {
$http.post('/rest/assignments/assignment/' + assignment.id + '/mark_elected/', {'user': user})
}
};
// Just mark some vote value strings for translation. // Just mark some vote value strings for translation.
gettext('Yes'), gettext('No'), gettext('Abstain'); gettext('Yes'), gettext('No'), gettext('Abstain');

View File

@ -42,8 +42,9 @@
<h3 translate>Candidates</h3> <h3 translate>Candidates</h3>
<ol> <ol>
<li ng-repeat="related_user in assignment.assignment_related_users" ng-if="related_user.status == 1"> <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> <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)" <button os-perms="assignments.can_manage" ng-click="removeCandidate(related_user.user_id)"
class="btn btn-default btn-xs"> class="btn btn-default btn-xs">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
@ -55,7 +56,7 @@
{{alert.msg}} {{alert.msg}}
</alert> </alert>
<div os-perms="assignments.can_nominate_other" class="input-group"> <div os-perms="assignments.can_nominate_other" class="input-group">
<ui-select ng-model="candidate.selected" ng-change="addCandidate(candidate.selected.id)"> <ui-select ng-model="candidateSelectBox.selected" ng-change="addCandidate(candidateSelectBox.selected.id)">
<ui-select-match placeholder="{{ 'Select or search a participant ...' | translate }}"> <ui-select-match placeholder="{{ 'Select or search a participant ...' | translate }}">
{{ $select.selected.get_full_name() }} {{ $select.selected.get_full_name() }}
</ui-select-match> </ui-select-match>
@ -64,7 +65,7 @@
</ui-select-choices> </ui-select-choices>
</ui-select> </ui-select>
<span class="input-group-btn"> <span class="input-group-btn">
<a ng-click="candidate={}" class="btn btn-default"> <a ng-click="candidateSelectBox={}" class="btn btn-default">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
</a> </a>
</span> </span>
@ -89,21 +90,24 @@
<uib-tabset class="spacer"> <uib-tabset class="spacer">
<uib-tab ng-repeat="poll in assignment.polls" heading="Ballot {{$index+1}}"> <uib-tab ng-repeat="poll in assignment.polls" heading="Ballot {{$index+1}}">
<div os-perms="assignments.can_manage" class="spacer"> <div os-perms="assignments.can_manage" class="spacer">
<button ng-click="editPollDialog(poll, $index+1)"
class="btn btn-default btn-sm">
<i class="fa fa-pencil"></i>
<translate>Edit</translate>
</button>
<!-- angular requires to open the link in new tab with "target='_blank'". <!-- angular requires to open the link in new tab with "target='_blank'".
Otherwise the pdf url can't be open in same window; angular redirects to "/". --> Otherwise the pdf url can't be open in same window; angular redirects to "/". -->
<a ui-sref="assignmentpoll_pdf({poll_pk: poll.id})" target="_blank" <a ui-sref="assignmentpoll_pdf({poll_pk: poll.id})" target="_blank"
class="btn btn-default btn-sm"> class="btn btn-default btn-sm">
<i class="fa fa-file-pdf-o"></i> Ballot paper <i class="fa fa-file-pdf-o"></i>
1. <translate>Print ballot paper</translate>
</a> </a>
<i class="fa fa-arrow-right"></i>
<button ng-click="editPollDialog(poll, $index+1)"
class="btn btn-default btn-sm">
<i class="fa fa-pencil"></i>
2. <translate>Enter votes</translate>
</button>
<i class="fa fa-arrow-right"></i>
<button os-perms-lite="assignments.can_manage" ng-if="!poll.published" ng-click="publishBallot(poll, true)" <button os-perms-lite="assignments.can_manage" ng-if="!poll.published" ng-click="publishBallot(poll, true)"
class="btn btn-default btn-sm"> class="btn btn-default btn-sm">
<i class="fa fa-toggle-off"></i> <i class="fa fa-toggle-off"></i>
<translate>Not published</translate> 3. <translate>Publish ballot</translate>
</button> </button>
<button os-perms-lite="assignments.can_manage" ng-if="poll.published" ng-click="publishBallot(poll, false)" <button os-perms-lite="assignments.can_manage" ng-if="poll.published" ng-click="publishBallot(poll, false)"
class="btn btn-default btn-sm"> class="btn btn-default btn-sm">
@ -115,23 +119,49 @@
<translate>Delete</translate> <translate>Delete</translate>
</a> </a>
</div> </div>
<div class="results"> <div class="results spacer">
<div ng-repeat="option in poll.options"> <table class="table table-bordered table-striped minimumTable">
<div ng-if="poll.yesnoabstain && option.votes.length > 0"> <tr>
<strong>{{ option.candidate.get_full_name() }}</strong><br> <th translate>Elected
{{ option.votes[0].value | translate }}: {{ option.votes[0].weight }}<br> <th translate>Candidates
{{ option.votes[1].value | translate }}: {{ option.votes[1].weight }}<br> <th ng-if="poll.has_votes" translate>Votes
{{ option.votes[2].value | translate }}: {{ option.votes[2].weight }} <tr ng-repeat="option in poll.options">
<hr class="smallhr"> <td class="minimum">
</div> <button os-perms="assignments.can_manage"
<div ng-if="!poll.yesnoabstain && option.votes.length > 0"> ng-click="markElected(option.candidate_id, option.is_elected)" class="btn btn-default btn-xs">
{{ option.candidate.get_full_name() }}: {{ option.votes[0].weight}} <i ng-if="option.is_elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
<hr class="smallhr"> <i ng-if="!option.is_elected" class="fa fa-star-o" title="{{ 'is not elected' | translate }}"></i>
</div> </button>
</div> <td>
Valid votes: {{ poll.votesvalid }}<br> <a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a>
Invalid votes: {{ poll.votesinvalid }}<br> <td ng-if="poll.has_votes">
Votes cast: {{ poll.votescast }} <span ng-if="poll.yesnoabstain && option.votes.length > 0">
{{ option.votes[0].value | translate }}: {{ option.votes[0].weight }}<br>
{{ option.votes[1].value | translate }}: {{ option.votes[1].weight }}<br>
{{ option.votes[2].value | translate }}: {{ option.votes[2].weight }}
</span>
<span ng-if="!poll.yesnoabstain && option.votes.length > 0">
{{ option.votes[0].weight}}
</span>
<tr>
<td>
<td>
<translate>Valid votes</translate>
<td ng-if="poll.has_votes">
{{ poll.votesvalid }}
<tr>
<td>
<td>
<translate>Invalid votes</translate>
<td ng-if="poll.has_votes">
{{ poll.votesinvalid }}
<tr class="total bg-info">
<td>
<td>
<translate>Votes cast</translate>
<td ng-if="poll.has_votes">
{{ poll.votescast }}
</table>
</div> </div>
</uib-tab> </uib-tab>
</uib-tabset> </uib-tabset>

View File

@ -146,8 +146,6 @@ class AssignmentViewSet(ModelViewSet):
""" """
user = self.get_user_from_request_data(request) user = self.get_user_from_request_data(request)
assignment = self.get_object() assignment = self.get_object()
if assignment.is_elected(user):
raise ValidationError({'detail': _('User %s is already elected.') % user})
if request.method == 'POST': if request.method == 'POST':
message = self.nominate_other(request, user, assignment) message = self.nominate_other(request, user, assignment)
else: else:
@ -156,15 +154,14 @@ class AssignmentViewSet(ModelViewSet):
return Response({'detail': message}) return Response({'detail': message})
def nominate_other(self, request, user, assignment): def nominate_other(self, request, user, assignment):
if assignment.is_elected(user):
raise ValidationError({'detail': _('User %s is already elected.') % user})
if assignment.phase == assignment.PHASE_FINISHED: if assignment.phase == assignment.PHASE_FINISHED:
detail = _('You can not nominate someone to this election because it is finished.') detail = _('You can not nominate someone to this election because it is finished.')
raise ValidationError({'detail': detail}) raise ValidationError({'detail': detail})
if assignment.phase == assignment.PHASE_VOTING and not request.user.has_perm('assignments.can_manage'): if assignment.phase == assignment.PHASE_VOTING and not request.user.has_perm('assignments.can_manage'):
# To nominate another user during voting you have to be a manager. # To nominate another user during voting you have to be a manager.
self.permission_denied(request) self.permission_denied(request)
if not request.user.has_perm('assignments.can_manage'):
if assignment.is_elected(user):
raise ValidationError({'detail': _('User %s is already elected.') % user})
if assignment.is_candidate(user): if assignment.is_candidate(user):
raise ValidationError({'detail': _('User %s is already nominated.') % user}) raise ValidationError({'detail': _('User %s is already nominated.') % user})
assignment.set_candidate(user) assignment.set_candidate(user)
@ -177,7 +174,7 @@ class AssignmentViewSet(ModelViewSet):
if assignment.phase == assignment.PHASE_FINISHED: if assignment.phase == assignment.PHASE_FINISHED:
detail = _('You can not delete someones candidature to this election because it is finished.') detail = _('You can not delete someones candidature to this election because it is finished.')
raise ValidationError({'detail': detail}) raise ValidationError({'detail': detail})
if not assignment.is_candidate(user): if not assignment.is_candidate(user) and not assignment.is_elected(user):
raise ValidationError({'detail': _('User %s has no status in this election.') % user}) raise ValidationError({'detail': _('User %s has no status in this election.') % user})
assignment.delete_related_user(user) assignment.delete_related_user(user)
return _('Candidate %s was withdrawn successfully.') % user return _('Candidate %s was withdrawn successfully.') % user

View File

@ -659,6 +659,9 @@ img {
.result_label { .result_label {
margin-top: 5px; margin-top: 5px;
} }
tr.total td {
font-weight: bold;
}
/* Chatbox */ /* Chatbox */
#chatbox { #chatbox {
@ -685,6 +688,10 @@ img {
width: 1px; width: 1px;
} }
.minimumTable {
width: auto;
}
.deleteColumn { .deleteColumn {
text-align: center; text-align: center;
background-color: #ff9999 !important; background-color: #ff9999 !important;