Mark candidate as elected. Updated assignment detail.

This commit is contained in:
Emanuel Schuetze 2016-01-09 19:27:02 +01:00
parent 1f72ef4099
commit 2c46d3ae0a
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) {
Agenda.bindOne(item.id, $scope, 'item');
User.bindAll({}, $scope, 'users');
$scope.speaker = {};
$scope.speakerSelectBox = {};
$scope.alert = {};
$scope.speakers = $filter('orderBy')(item.speakers, 'weight');
$scope.$watch(function () {
@ -208,9 +208,11 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
.success(function(data){
$scope.alert.show = false;
$scope.speakers = item.speakers;
$scope.speakerSelectBox = {};
})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
$scope.speakerSelectBox = {};
});
};
// delete speaker(!) from list of speakers

View File

@ -89,7 +89,7 @@
{{alert.msg}}
</alert>
<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 }}">
{{ $select.selected.get_full_name() }}
</ui-select-match>
@ -98,7 +98,7 @@
</ui-select-choices>
</ui-select>
<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>
</a>
</span>

View File

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

View File

@ -245,16 +245,18 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
User.bindAll({}, $scope, 'users');
Assignment.bindOne(assignment.id, $scope, 'assignment');
Assignment.loadRelations(assignment, 'agenda_item');
$scope.candidate = {};
$scope.candidateSelectBox = {};
$scope.alert = {};
// add (nominate) candidate
$scope.addCandidate = function (userId) {
$http.post('/rest/assignments/assignment/' + assignment.id + '/candidature_other/', {'user': userId})
.success(function(data){
$scope.alert.show = false;
$scope.candidateSelectBox = {};
})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
$scope.candidateSelectBox = {};
});
};
// remove candidate
@ -266,15 +268,6 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
$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)
$scope.addMe = function () {
$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 };
});
};
// delete ballt
// delete ballot
$scope.deleteBallot = function (poll) {
poll.DSDestroy();
}
@ -353,6 +346,18 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
$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.
gettext('Yes'), gettext('No'), gettext('Abstain');

View File

@ -42,8 +42,9 @@
<h3 translate>Candidates</h3>
<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>
<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>
@ -55,7 +56,7 @@
{{alert.msg}}
</alert>
<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 }}">
{{ $select.selected.get_full_name() }}
</ui-select-match>
@ -64,7 +65,7 @@
</ui-select-choices>
</ui-select>
<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>
</a>
</span>
@ -89,21 +90,24 @@
<uib-tabset class="spacer">
<uib-tab ng-repeat="poll in assignment.polls" heading="Ballot {{$index+1}}">
<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'".
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"
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>
<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)"
class="btn btn-default btn-sm">
<i class="fa fa-toggle-off"></i>
<translate>Not published</translate>
3. <translate>Publish ballot</translate>
</button>
<button os-perms-lite="assignments.can_manage" ng-if="poll.published" ng-click="publishBallot(poll, false)"
class="btn btn-default btn-sm">
@ -115,23 +119,49 @@
<translate>Delete</translate>
</a>
</div>
<div class="results">
<div ng-repeat="option in poll.options">
<div ng-if="poll.yesnoabstain && option.votes.length > 0">
<strong>{{ option.candidate.get_full_name() }}</strong><br>
{{ 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 }}
<hr class="smallhr">
</div>
<div ng-if="!poll.yesnoabstain && option.votes.length > 0">
{{ option.candidate.get_full_name() }}: {{ option.votes[0].weight}}
<hr class="smallhr">
</div>
</div>
Valid votes: {{ poll.votesvalid }}<br>
Invalid votes: {{ poll.votesinvalid }}<br>
Votes cast: {{ poll.votescast }}
<div class="results spacer">
<table class="table table-bordered table-striped minimumTable">
<tr>
<th translate>Elected
<th translate>Candidates
<th ng-if="poll.has_votes" translate>Votes
<tr ng-repeat="option in poll.options">
<td class="minimum">
<button os-perms="assignments.can_manage"
ng-click="markElected(option.candidate_id, option.is_elected)" class="btn btn-default btn-xs">
<i ng-if="option.is_elected" class="fa fa-star" title="{{ 'is elected' | translate }}"></i>
<i ng-if="!option.is_elected" class="fa fa-star-o" title="{{ 'is not elected' | translate }}"></i>
</button>
<td>
<a ui-sref="users.user.detail({id: option.candidate.id})">{{ option.candidate.get_full_name() }}</a>
<td ng-if="poll.has_votes">
<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>
</uib-tab>
</uib-tabset>

View File

@ -146,8 +146,6 @@ class AssignmentViewSet(ModelViewSet):
"""
user = self.get_user_from_request_data(request)
assignment = self.get_object()
if assignment.is_elected(user):
raise ValidationError({'detail': _('User %s is already elected.') % user})
if request.method == 'POST':
message = self.nominate_other(request, user, assignment)
else:
@ -156,15 +154,14 @@ class AssignmentViewSet(ModelViewSet):
return Response({'detail': message})
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:
detail = _('You can not nominate someone to this election because it is finished.')
raise ValidationError({'detail': detail})
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.
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):
raise ValidationError({'detail': _('User %s is already nominated.') % user})
assignment.set_candidate(user)
@ -177,7 +174,7 @@ class AssignmentViewSet(ModelViewSet):
if assignment.phase == assignment.PHASE_FINISHED:
detail = _('You can not delete someones candidature to this election because it is finished.')
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})
assignment.delete_related_user(user)
return _('Candidate %s was withdrawn successfully.') % user

View File

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