Merge pull request #1815 from emanuelschuetze/candidateElected
Mark candidate as elected. Updated assignment detail.
This commit is contained in:
commit
c30116f5c1
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user