Merge pull request #1709 from emanuelschuetze/assignments-rework

Assignments template improvements
This commit is contained in:
Oskar Hahn 2015-11-27 20:16:41 +01:00
commit 6a4cc97469
14 changed files with 680 additions and 142 deletions

View File

@ -161,7 +161,6 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
} }
$scope.alert = { type: 'danger', msg: message, show: true }; $scope.alert = { type: 'danger', msg: message, show: true };
}); });
;
}; };
// delete related item // delete related item
$scope.deleteRelatedItem = function (item) { $scope.deleteRelatedItem = function (item) {

View File

@ -1,7 +1,7 @@
<h1 translate>Agenda</h1> <h1 translate>Agenda</h1>
<div id="submenu"> <div id="submenu">
<a ng-click="newDialog()" ng-dialog-class="ngdialog-theme-plain"os-perms="agenda.can_manage" class="btn btn-primary btn-sm"> <a ng-click="newDialog()" os-perms="agenda.can_manage" class="btn btn-primary btn-sm">
<i class="fa fa-plus fa-lg"></i> <i class="fa fa-plus fa-lg"></i>
<translate>New</translate> <translate>New</translate>
</a> </a>

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assignments', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='assignmentoption',
name='poll',
field=models.ForeignKey(to='assignments.AssignmentPoll', related_name='options'),
),
migrations.AlterField(
model_name='assignmentvote',
name='option',
field=models.ForeignKey(to='assignments.AssignmentOption', related_name='votes'),
),
]

View File

@ -106,7 +106,7 @@ class Assignment(RESTModelMixin, models.Model):
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
through='AssignmentRelatedUser') through='AssignmentRelatedUser')
""" """
Users that a candidates, elected or blocked as candidate. Users that are candidates, elected or blocked as candidate.
See AssignmentRelatedUser for more infos. See AssignmentRelatedUser for more infos.
""" """
@ -180,7 +180,7 @@ class Assignment(RESTModelMixin, models.Model):
def is_blocked(self, user): def is_blocked(self, user):
""" """
Returns True if the user is blockt for candidature. Returns True if the user is blocked for candidature.
Costs one database query. Costs one database query.
""" """
@ -253,12 +253,11 @@ class Assignment(RESTModelMixin, models.Model):
yesnoabstain=yesnoabstain) yesnoabstain=yesnoabstain)
poll.set_options({'candidate': user} for user in candidates) poll.set_options({'candidate': user} for user in candidates)
# Add all candidates to all agenda items for this assignment # Add all candidates to list of speakers of related agenda item
# TODO: Try to do this in a bulk create # TODO: Try to do this in a bulk create
for item in self.items.all():
for candidate in self.candidates: for candidate in self.candidates:
try: try:
Speaker.objects.add(candidate, item) Speaker.objects.add(candidate, self.agenda_item)
except OpenSlidesError: except OpenSlidesError:
# The Speaker is already on the list. Do nothing. # The Speaker is already on the list. Do nothing.
# TODO: Find a smart way not to catch the error concerning AnonymousUser. # TODO: Find a smart way not to catch the error concerning AnonymousUser.
@ -320,7 +319,7 @@ class Assignment(RESTModelMixin, models.Model):
class AssignmentVote(RESTModelMixin, BaseVote): class AssignmentVote(RESTModelMixin, BaseVote):
option = models.ForeignKey('AssignmentOption') option = models.ForeignKey('AssignmentOption', related_name='votes')
def get_root_rest_element(self): def get_root_rest_element(self):
""" """
@ -330,7 +329,7 @@ class AssignmentVote(RESTModelMixin, BaseVote):
class AssignmentOption(RESTModelMixin, BaseOption): class AssignmentOption(RESTModelMixin, BaseOption):
poll = models.ForeignKey('AssignmentPoll') poll = models.ForeignKey('AssignmentPoll', related_name='options')
candidate = models.ForeignKey(settings.AUTH_USER_MODEL) candidate = models.ForeignKey(settings.AUTH_USER_MODEL)
vote_class = AssignmentVote vote_class = AssignmentVote

View File

@ -45,8 +45,8 @@ class AssignmentSlide(ProjectorElement):
view_class=user.get_view_class(), view_class=user.get_view_class(),
view_action='retrieve', view_action='retrieve',
pk=str(user.pk)) pk=str(user.pk))
for poll in assignment.polls.all().prefetch_related('assignmentoption_set'): for poll in assignment.polls.all().prefetch_related('options'):
for option in poll.assignmentoption_set.all(): for option in poll.options.all():
yield ProjectorRequirement( yield ProjectorRequirement(
view_class=option.candidate.get_view_class(), view_class=option.candidate.get_view_class(),
view_action='retrieve', view_action='retrieve',

View File

@ -46,11 +46,11 @@ class AssignmentOptionSerializer(ModelSerializer):
""" """
Serializer for assignment.models.AssignmentOption objects. Serializer for assignment.models.AssignmentOption objects.
""" """
assignmentvote_set = AssignmentVoteSerializer(many=True, read_only=True) votes = AssignmentVoteSerializer(many=True, read_only=True)
class Meta: class Meta:
model = AssignmentOption model = AssignmentOption
fields = ('candidate', 'assignmentvote_set',) fields = ('candidate', 'votes',)
class FilterPollListSerializer(ListSerializer): class FilterPollListSerializer(ListSerializer):
@ -75,7 +75,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
Serializes all polls. Serializes all polls.
""" """
assignmentoption_set = AssignmentOptionSerializer(many=True, read_only=True) options = AssignmentOptionSerializer(many=True, read_only=True)
votes = ListField( votes = ListField(
child=DictField( child=DictField(
child=IntegerField(min_value=-2)), child=IntegerField(min_value=-2)),
@ -88,7 +88,7 @@ class AssignmentAllPollSerializer(ModelSerializer):
'yesnoabstain', 'yesnoabstain',
'description', 'description',
'published', 'published',
'assignmentoption_set', 'options',
'votesvalid', 'votesvalid',
'votesinvalid', 'votesinvalid',
'votescast', 'votescast',
@ -148,7 +148,7 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
'yesnoabstain', 'yesnoabstain',
'description', 'description',
'published', 'published',
'assignmentoption_set', 'options',
'votesvalid', 'votesvalid',
'votesinvalid', 'votesinvalid',
'votescast',) 'votescast',)

View File

@ -9,7 +9,7 @@ angular.module('OpenSlidesApp.assignments', [])
'Config', 'Config',
function (DS, Config) { function (DS, Config) {
return DS.defineResource({ return DS.defineResource({
name: 'assignments/poll', name: 'assignments/assignmentpoll',
relations: { relations: {
belongsTo: { belongsTo: {
'assignments/assignment': { 'assignments/assignment': {
@ -44,12 +44,13 @@ angular.module('OpenSlidesApp.assignments', [])
'AssignmentRelatedUser', 'AssignmentRelatedUser',
'AssignmentPoll', 'AssignmentPoll',
'jsDataModel', 'jsDataModel',
function (DS, AssignmentRelatedUser, AssignmentPoll, jsDataModel) { 'gettext',
function (DS, AssignmentRelatedUser, AssignmentPoll, jsDataModel, gettext) {
var name = 'assignments/assignment'; var name = 'assignments/assignment';
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
useClass: jsDataModel, useClass: jsDataModel,
agendaSupplement: '(Assignment)', agendaSupplement: '(' + gettext('Election') + ')',
methods: { methods: {
getResourceName: function () { getResourceName: function () {
return name; return name;
@ -73,6 +74,10 @@ angular.module('OpenSlidesApp.assignments', [])
'assignments/relateduser': { 'assignments/relateduser': {
localField: 'assignment_related_users', localField: 'assignment_related_users',
foreignKey: 'assignment_id', foreignKey: 'assignment_id',
},
'assignments/assignmentpoll': {
localField: 'polls',
foreignKey: 'assignment_id',
} }
} }
} }

View File

@ -44,6 +44,9 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
resolve: { resolve: {
assignment: function(Assignment, $stateParams) { assignment: function(Assignment, $stateParams) {
return Assignment.find($stateParams.id); return Assignment.find($stateParams.id);
},
users: function(User) {
return User.findAll();
} }
} }
}) })
@ -54,10 +57,93 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
}); });
}) })
.controller('AssignmentListCtrl', function($scope, Assignment, phases) { // Provide generic assignment form fields for create and update view
.factory('AssignmentFormFieldFactory', [
'gettext',
function (gettext) {
return {
getFormFields: function () {
return [
{
key: 'title',
type: 'input',
templateOptions: {
label: gettext('Title'),
required: true
}
},
{
key: 'description',
type: 'textarea',
templateOptions: {
label: gettext('Description')
}
},
{
key: 'open_posts',
type: 'input',
templateOptions: {
label: gettext('Number of members to be elected'),
type: 'number',
required: true
}
},
{
key: 'poll_description_default',
type: 'input',
templateOptions: {
label: gettext('Default comment on the ballot paper')
}
}];
}
}
}
])
// Provide generic assignmentpoll form fields for create and update view
.factory('AssignmentPollFormFieldFactory', [
'gettext',
function (gettext) {
return {
getFormFields: function () {
return [
{
key: 'description',
type: 'input',
templateOptions: {
label: gettext('Comment on the ballot paper')
}
},
{
key: 'yes',
type: 'input',
templateOptions: {
label: gettext('Yes'),
type: 'number',
required: true
}
},
{
key: 'poll_description_default',
type: 'input',
templateOptions: {
label: gettext('Default comment on the ballot paper')
}
}];
}
}
}
])
.controller('AssignmentListCtrl', [
'$scope',
'ngDialog',
'Assignment',
'phases',
function($scope, ngDialog, Assignment, phases) {
Assignment.bindAll({}, $scope, 'assignments'); Assignment.bindAll({}, $scope, 'assignments');
// get all item types via OPTIONS request // get all item types via OPTIONS request
$scope.phases = phases.data.actions.POST.phase.choices; $scope.phases = phases.data.actions.POST.phase.choices;
$scope.alert = {};
// setup table sorting // setup table sorting
$scope.sortColumn = 'title'; $scope.sortColumn = 'title';
@ -71,37 +157,323 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
$scope.sortColumn = column; $scope.sortColumn = column;
}; };
// delete selected assignment // open new customslide dialog
$scope.delete = function (assignment) { $scope.newDialog = function () {
Assignment.destroy(assignment.id); ngDialog.open({
template: 'static/templates/assignments/assignment-form.html',
controller: 'AssignmentCreateCtrl',
className: 'ngdialog-theme-default wide-form'
});
}; };
}) // edit view of related item (content object)
$scope.editDialog = function (assignment) {
.controller('AssignmentDetailCtrl', function($scope, Assignment, assignment) { ngDialog.open({
Assignment.bindOne(assignment.id, $scope, 'assignment'); template: 'static/templates/assignments/assignment-form.html',
Assignment.loadRelations(assignment, 'agenda_item'); controller: 'AssignmentUpdateCtrl',
}) className: 'ngdialog-theme-default wide-form',
resolve: {
.controller('AssignmentCreateCtrl', function($scope, $state, Assignment) { assignment: function(Assignment) {
$scope.assignment = {}; return Assignment.find(assignment.id);
$scope.save = function(assignment) {
Assignment.create(assignment).then(
function(success) {
$state.go('assignments.assignment.list');
} }
); }
});
}; };
}) // save changed item
.controller('AssignmentUpdateCtrl', function($scope, $state, Assignment, assignment) {
$scope.assignment = assignment; // do not use .binOne(...) so autoupdate is not activated
$scope.save = function (assignment) { $scope.save = function (assignment) {
Assignment.save(assignment).then( Assignment.save(assignment).then(
function(success) { function(success) {
$state.go('assignments.assignment.list'); assignment.quickEdit = false;
$scope.alert.show = false;
},
function(error){
var message = '';
for (var e in error.data) {
message += e + ': ' + error.data[e] + ' ';
}
$scope.alert = { type: 'danger', msg: message, show: true };
});
};
// delete all selected assignments
$scope.deleteMultiple = function () {
angular.forEach($scope.assignments, function (assignment) {
if (assignment.selected)
Assignment.destroy(assignment.id);
});
$scope.isDeleteMode = false;
$scope.uncheckAll();
};
// delete single assignment
$scope.delete = function (assignment) {
Assignment.destroy(assignment.id);
};
}
])
.controller('AssignmentDetailCtrl', [
'$scope',
'$http',
'ngDialog',
'operator',
'Assignment',
'User',
'assignment',
function($scope, $http, ngDialog, operator, Assignment, User, assignment) {
User.bindAll({}, $scope, 'users');
Assignment.bindOne(assignment.id, $scope, 'assignment');
Assignment.loadRelations(assignment, 'agenda_item');
$scope.candidate = {};
$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;
})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// remove candidate
$scope.removeCandidate = 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 };
});
};
// 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/', {})
.success(function(data){
$scope.alert.show = false;
})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// remove me (withdraw own candidature)
$scope.removeMe = function () {
$http.delete('/rest/assignments/assignment/' + assignment.id + '/candidature_self/')
.success(function(data){
$scope.alert.show = false;
})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// check if current user is already a candidate (status=1)
$scope.isCandidate = function () {
var check = assignment.assignment_related_users.map( function(candidate) {
if ( candidate.status == 1)
return candidate.user_id;
}).indexOf(operator.user.id);
if (check > -1)
return true;
else
return false;
};
// create new ballot
$scope.createBallot = function () {
$http.post('/rest/assignments/assignment/' + assignment.id + '/create_poll/')
.success(function(data){
$scope.alert.show = false;
})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
// delete ballt
$scope.deleteBallot = function (poll) {
poll.DSDestroy();
}
// edit poll dialog
$scope.editPollDialog = function (poll) {
ngDialog.open({
template: 'static/templates/assignments/assignmentpoll-form.html',
controller: 'AssignmentPollUpdateCtrl',
className: 'ngdialog-theme-default',
resolve: {
assignmentpoll: function(AssignmentPoll) {
return AssignmentPoll.find(poll.id);
}
}
});
};
// publish ballot
$scope.publishBallot = function () {
// TODO poll.DSUpdate()
$http.put('/rest/assignments/assignment/' + assignment.id + '/publish_poll/')
.success(function(data){
$scope.alert.show = false;
})
.error(function(data){
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
}
])
.controller('AssignmentCreateCtrl', [
'$scope',
'$state',
'Assignment',
'AssignmentFormFieldFactory',
function($scope, $state, Assignment, AssignmentFormFieldFactory) {
$scope.assignment = {};
// get all form fields
$scope.formFields = AssignmentFormFieldFactory.getFormFields();
// save assignment
$scope.save = function(assignment) {
Assignment.create(assignment).then(
function(success) {
$scope.closeThisDialog();
} }
); );
}; };
}); }
])
.controller('AssignmentUpdateCtrl', [
'$scope',
'$state',
'Assignment',
'AssignmentFormFieldFactory',
'assignment',
function($scope, $state, Assignment, AssignmentFormFieldFactory, assignment) {
// set initial values for form model
$scope.model = assignment;
// get all form fields
$scope.formFields = AssignmentFormFieldFactory.getFormFields();
// save assignment
$scope.save = function (assignment) {
Assignment.save(assignment).then(
function(success) {
$scope.closeThisDialog();
}
);
};
}
])
.controller('AssignmentPollUpdateCtrl', [
'$scope',
'$state',
'gettext',
'AssignmentPoll',
'assignmentpoll',
function($scope, $state, gettext, AssignmentPoll, assignmentpoll) {
// set initial values for form model
$scope.model = assignmentpoll;
$scope.formFields = [];
// add dynamic form fields
assignmentpoll.options.forEach(function(option) {
$scope.formFields.push(
{
noFormControl: true,
template: '<strong>User#' + option.candidate_id + '</strong>'
},
{
key: 'yes_' + option.candidate_id,
type: 'input',
templateOptions: {
label: gettext('Yes'),
type: 'number',
required: true
}
},
{
key: 'no_' + option.candidate_id,
type: 'input',
templateOptions: {
label: gettext('No'),
type: 'number',
required: true
}
},
{
key:'abstain_' + option.candidate_id,
type: 'input',
templateOptions: {
label: gettext('Abstain'),
type: 'number',
required: true
}
}
);
});
// add general form fields
$scope.formFields.push(
{
key: 'votesvalid',
type: 'input',
templateOptions: {
label: gettext('Votes valid'),
type: 'number'
}
},
{
key: 'votesinvalid',
type: 'input',
templateOptions: {
label: gettext('Votes invalid'),
type: 'number'
}
},
{
key: 'votescast',
type: 'input',
templateOptions: {
label: gettext('Votes cast'),
type: 'number'
}
},
// TODO: update description in separat request
// (without vote result values)
{
key: 'description',
type: 'input',
templateOptions: {
label: gettext('Comment on the ballot paper')
}
}
);
// save assignment
$scope.save = function (poll) {
var votes = [];
assignmentpoll.options.forEach(function(option) {
votes.push({
"Yes": poll['yes_' + option.candidate_id],
"No": poll['no_' + option.candidate_id],
"Abstain": poll['abstain_' + option.candidate_id]
});
});
poll.DSUpdate({
assignment_id: poll.assignment_id,
votes: votes,
votesvalid: poll.votesvalid,
votesinvalid: poll.votesinvalid,
votescast: poll.votescast
})
.then(function(success) {
$scope.closeThisDialog();
})
};
}
]);
}()); }());

View File

@ -24,18 +24,98 @@
</a> </a>
</div> </div>
Agenda Item: {{ assignment.agenda_item }}
<h3 translate>Description</h3> <h3 translate>Description</h3>
<div class="white-space-pre-line">{{ assignment.description }}</div> <div class="white-space-pre-line">{{ assignment.description }}</div>
<h3 translate>Candidates</h3> <h3 translate>Candidates</h3>
<ul> <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" ng-if="related_user.status == 1">
User: {{ related_user.user.get_full_name() }} <a ui-sref="users.user.detail({id: related_user.user_id})">{{ related_user.user.get_full_name() }}</a>
</li> <button os-perms="assignments.can_manage" ng-click="removeCandidate(related_user.user_id)"
</ul> class="btn btn-default btn-xs">
<i class="fa fa-times"></i>
</button>
</ol>
<div class="form-group">
<alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
{{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-match placeholder="{{ 'Select or search a participant...' | translate }}">
{{ $select.selected.get_full_name() }}
</ui-select-match>
<ui-select-choices repeat="user in users | filter: $select.search">
<div ng-bind-html="user.get_full_name() | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<span class="input-group-btn">
<a ng-click="candidate={}" class="btn btn-default">
<i class="fa fa-times-circle"></i>
</a>
</span>
</div>
<p os-perm="assignments.can_nominate_self">
<button ng-if="!isCandidate()" ng-click="addMe()" class="btn btn-default">
<i class="fa fa-plus"></i>
<translate>Add me</translate>
</button>
<button ng-if="isCandidate()" ng-click="removeMe()" class="btn btn-default">
<i class="fa fa-minus"></i>
<translate>Remove me</translate>
</button>
</div>
<h3 translate>Election result</h3> <h3 translate>Election result</h3>
<!-- TODO --> <button os-perms="assignments.can_manage" ng-click="createBallot()" class="btn btn-default btn-sm">
<i class="fa fa-bar-chart fa-lg"></i>
<translate>New ballot</translate>
</button>
<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)"
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
</a>
<button os-perms-lite="assignments.can_manage" ng-if="!poll.published" ng-click="publishBallot(poll)"
class="btn btn-primary btn-sm">
<i class="fa fa-globe"></i>
<translate>Publish result</translate>
</button>
<button os-perms-lite="assignments.can_manage" ng-if="poll.published" ng-click="unpublishBallot(poll)"
class="btn btn-default btn-sm">
<i class="fa fa-globe"></i>
<translate>Unpublish result</translate>
</button>
<a ng-click="deleteBallot(poll)" class="btn btn-default btn-sm">
<i class="fa fa-times"></i>
<translate>Delete</translate>
</a>
</div>
<div class="results">
<div ng-repeat="option in poll.options">
<strong>User#{{ option.candidate_id }}</strong>
<div ng-if="option.votes.length > 0">
<div ng-repeat="vote in option.votes">
{{ vote.value}}: {{ vote.weight}}
</div>
</div>
</div>
<hr>
Valid votes: {{ poll.votesvalid }}<br>
Invalid votes: {{ poll.votesinvalid }}<br>
Votes cast: {{ poll.votescast }}
</div>
</uib-tab>
</uib-tabset>

View File

@ -1,31 +1,13 @@
<h1 ng-if="assignment.id" translate>Edit election</h1> <h1 ng-if="assignment.id" translate>Edit election</h1>
<h1 ng-if="!assignment.id" translate>New election</h1> <h1 ng-if="!assignment.id" translate>New election</h1>
<div id="submenu"> <form name="assignmentForm" ng-submit="save(model)">
<a ui-sref="assignments.assignment.list" class="btn btn-sm btn-default"> <formly-form model="model" fields="formFields">
<i class="fa fa-angle-double-left fa-lg"></i> <button type="submit" ng-disabled="assignmentForm.$invalid" class="btn btn-primary" translate>
<translate>Back to overview</translate> Submit
</a>
</div>
<form name="assignmentForm">
<div class="form-group">
<label for="inputTitle" translate>Title</label>
<input type="text" ng-model="assignment.title" class="form-control" name="inputTitle" required>
</div>
<div class="form-group">
<label for="textareaDesciption" translate>Description</label>
<textarea ng-model="assignment.description" class="form-control" name="textareaDescription" />
</div>
<div class="form-group">
<label for="inputPosts" translate>Number of members to be elected</label>
<input type="number" ng-model="assignment.open_posts" class="form-control" name="inputPosts" required>
</div>
<button type="submit" ng-click="save(assignment)" class="btn btn-primary" translate>
Save
</button> </button>
<button ui-sref="assignments.assignment.list" class="btn btn-default" translate> <button ng-click="closeThisDialog()" class="btn btn-default" translate>
Cancel Cancel
</button> </button>
</formly-form>
</form> </form>

View File

@ -1,7 +1,7 @@
<h1 translate>Elections</h1> <h1 translate>Elections</h1>
<div id="submenu"> <div id="submenu">
<a ui-sref="assignments.assignment.create" os-perms="assignments.can_manage" class="btn btn-primary btn-sm"> <a ng-click="newDialog()" os-perms="assignments.can_manage" class="btn btn-primary btn-sm">
<i class="fa fa-plus fa-lg"></i> <i class="fa fa-plus fa-lg"></i>
<translate>New</translate> <translate>New</translate>
</a> </a>
@ -17,64 +17,129 @@
<div class="row form-group"> <div class="row form-group">
<div class="col-sm-8"> <div class="col-sm-8">
<!-- TODO: select filter for phases --> <form class="form-inline">
<!-- delete mode -->
<div os-perms-lite="assignments.can_manage" class="form-group">
<label for="deleteSwitcher" translate>Delete mode</label>
<switch id="deleteSwitcher" ng-model="isDeleteMode" ng-change="uncheckAll()"
on="{{'On'|translate}}" off="{{'Off'|translate}}"
class="green wide form-control">
</switch>
</div>
<!-- delete button -->
<a ng-show="isDeleteMode && (assignments|filter:{selected:true}).length > 0"
os-perms="assignments.can_manage" ng-click="deleteMultiple()"
class="btn btn-primary btn-sm form-control">
<i class="fa fa-trash fa-lg"></i>
<translate>Delete selected elections</translate>
</a>
<!-- phase filter -->
&nbsp;
<select ng-model="phaseFilter" class="form-control" id="phaseFilter">
<option value="" translate>--- Select phase ---</option>
<option ng-repeat="phase in phases" value="{{ phase.value }}">{{ phase.display_name }}</option>
</select>
</form>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-filter"></i></div>
<input type="text" os-focus-me ng-model="filter.search" class="form-control" <input type="text" os-focus-me ng-model="filter.search" class="form-control"
placeholder="{{ 'Filter' | translate}}"> placeholder="{{ 'Filter' | translate}}">
</div> </div>
</div>
</div> </div>
<table class="table table-striped table-bordered table-hover"> <table class="table table-striped table-bordered table-hover">
<thead> <thead>
<tr> <tr>
<!-- projector column -->
<th ng-show="!isDeleteMode" os-perms="core.can_manage_projector" class="firstColumn">
<!-- delete selection column -->
<th ng-show="isDeleteMode" os-perms-lite="assignments.can_manage" class="firstColumn deleteColumn">
<input type="checkbox" ng-model="selectedAll" ng-change="checkAll()">
<th ng-click="toggleSort('title')" class="sortable"> <th ng-click="toggleSort('title')" class="sortable">
<translate>Title</translate> <translate>Title</translate>
<i class="pull-right fa" ng-show="sortColumn === 'title' && header.sortable != false" <i class="pull-right fa" ng-show="sortColumn === 'title' && header.sortable != false"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i> </i>
<th ng-click="toggleSort('open_posts')" class="sortable"> <th ng-click="toggleSort('open_posts')" class="sortable optional">
<translate>Posts</translate> <translate>Candidates</translate> / <translate>Posts</translate>
<i class="pull-right fa" ng-show="sortColumn === 'open_posts' && header.sortable != false" <i class="pull-right fa" ng-show="sortColumn === 'open_posts' && header.sortable != false"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i> </i>
<th ng-click="toggleSort('phase')" class="sortable"> <th ng-click="toggleSort('phase')" class="sortable optional">
<translate>State</translate> <translate>Phase</translate>
<i class="pull-right fa" ng-show="sortColumn === 'phase' && header.sortable != false" <i class="pull-right fa" ng-show="sortColumn === 'phase' && header.sortable != false"
ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'"> ng-class="reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i> </i>
<th os-perms="assignments.can_manage core.can_manage_projector" class="minimum">
<translate>Actions</translate>
<tbody> <tbody>
<tr ng-repeat="assignment in assignments | filter: filter.search | <tr ng-repeat="assignment in assignments | filter: filter.search | filter: {phase: phaseFilter} |
orderBy: sortColumn:reverse" ng-class="{ 'activeline': assignment.isProjected() }"> orderBy: sortColumn:reverse"
<td><a ui-sref="assignments.assignment.detail({id: assignment.id})">{{ assignment.title }}</a> class="animate-item"
<td class="optional">{{ assignment.open_posts }} ng-class="{ 'activeline': assignment.isProjected(), 'selected': assignment.selected }">
<td class="optional"> <!-- projector column -->
<td ng-show="!isDeleteMode" os-perms-lite="core.can_manage_projector">
<a class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': assignment.isProjected() }"
ng-click="assignment.project()"
title="{{ 'Project assignment' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<!-- delete selection column -->
<td ng-show="isDeleteMode" os-perms="assignments.can_manage" class="deleteColumn">
<input type="checkbox" ng-model="assignment.selected">
<!-- assignment data colums -->
<td ng-if="!assignment.quickEdit" ng-mouseover="assignment.hover=true" ng-mouseleave="assignment.hover=false">
<strong><a ui-sref="assignments.assignment.detail({id: assignment.id})">{{ assignment.title }}</a></strong>
<div os-perms="assignments.can_manage" class="hoverActions" ng-class="{'hiddenDiv': !assignment.hover}">
<a href="" ng-click="editDialog(assignment)" translate>Edit</a> |
<a href="" ng-click="assignment.quickEdit=true" translate>QuickEdit</a> |
<!-- TODO: translate confirm message -->
<a href="" class="text-danger"
ng-bootbox-confirm="Are you sure you want to delete <b>{{ assignment.title }}</b>?"
ng-bootbox-confirm-action="delete(assignment)" translate>Delete</a>
</div>
<td ng-if="!assignment.quickEdit" class="optional"><span class="badge">{{ assignment.open_posts }}</span>
<td ng-if="!assignment.quickEdit" class="optional">
<span class="label" ng-class="{'label-primary': assignment.phase == 0, <span class="label" ng-class="{'label-primary': assignment.phase == 0,
'label-warning': assignment.phase == 1, 'label-warning': assignment.phase == 1,
'label-success': assignment.phase == 2 }"> 'label-success': assignment.phase == 2 }">
{{ phases[assignment.phase].display_name }} {{ phases[assignment.phase].display_name }}
</span> </span>
<td os-perms="assignments.can_manage core.can_manage_projector" class="nobr"> <!-- quickEdit columns -->
<!-- project --> <td ng-if="assignment.quickEdit" colspan="3">
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm" <h4>{{ assignment.title }} <span class="text-muted">&ndash; Quick Edit</span></h4>
ng-class="{ 'btn-primary': assignment.isProjected() }" <alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
ng-click="assignment.project()" {{alert.msg}}
title="{{ 'Project election' | translate }}"> </alert>
<i class="fa fa-video-camera"></i> <div class="row">
</a> <div class="col-xs-6">
<!-- edit --> <label for="inputTitle" translate>Title</label>
<a ui-sref="assignments.assignment.detail.update({id: assignment.id })" os-perms="assignments.can_manage" <input type="text" ng-model="assignment.title" class="form-control input-sm" id="inputTitle">
class="btn btn-default btn-sm" </div>
title="{{ 'Edit' | translate}}"> <div class="col-xs-6">
<i class="fa fa-pencil"></i> <label for="inputPosts" translate>Number of members to be elected</label>
</a> <input type="number" ng-model="assignment.open_posts" class="form-control input-sm" id="inputPosts">
<!-- delete --> </div>
<a os-perms="assignments.can_manage" class="btn btn-danger btn-sm" </div>
ng-bootbox-confirm="Are you sure you want to delete <b>{{ assignment.title }}</b>?" <div class="row">
ng-bootbox-confirm-action="delete(assignment)" <div class="col-xs-6">
title="{{ 'Delete' | translate }}"> <label for="selectPhase" translate>Phase</label>
<i class="fa fa-trash-o"></i> <select ng-options="phase.value as phase.display_name for phase in phases"
</a> ng-model="assignment.phase" class="form-control" id="selectPhase">
</select>
</div>
<div class="col-xs-6"></div>
</div>
<div class="spacer">
<button ng-click="assignment.quickEdit=false" class="btn btn-default pull-left" translate>
Cancel
</button> &nbsp;
<button ng-click="save(assignment)" class="btn btn-primary" translate>
Update
</button>
<a href="" ng-click="editDialog(assignment)"
class="pull-right" translate>Edit election...</a>
</div>
</table> </table>

View File

@ -0,0 +1,12 @@
<h1 translate>Update ballot</h1>
<form name="assignmentpollForm" ng-submit="save(model)">
<formly-form model="model" fields="formFields">
<button type="submit" ng-disabled="assignmentForm.$invalid" class="btn btn-primary" translate>
Submit
</button>
<button ng-click="closeThisDialog()" class="btn btn-default" translate>
Cancel
</button>
</formly-form>
</form>

View File

@ -166,7 +166,7 @@ class AssignmentViewSet(ModelViewSet):
self.permission_denied(request) self.permission_denied(request)
if not request.user.has_perm('assignments.can_manage'): if not request.user.has_perm('assignments.can_manage'):
if assignment.is_blocked(user): if assignment.is_blocked(user):
raise ValidationError({'detail': _('User %s does not want to be an candidate.') % user}) raise ValidationError({'detail': _('User %s does not want to be a candidate. Only a manager can do this.') % user})
if assignment.is_elected(user): if assignment.is_elected(user):
raise ValidationError({'detail': _('User %s is already elected.') % user}) raise ValidationError({'detail': _('User %s is already elected.') % user})
# If the user is already a candidate he can be nominated nevertheless. # If the user is already a candidate he can be nominated nevertheless.
@ -344,7 +344,7 @@ class AssignmentPDF(PDFView):
length = len(vote_results) length = len(vote_results)
for candidate, poll_list in vote_results.iteritems(): for candidate, poll_list in vote_results.iteritems():
row = [] row = []
candidate_string = candidate.clean_name candidate_string = candidate.get_short_name()
if candidate in elected_candidates: if candidate in elected_candidates:
candidate_string = "* " + candidate_string candidate_string = "* " + candidate_string
if candidate.name_suffix and length < 20: if candidate.name_suffix and length < 20:
@ -549,7 +549,7 @@ class AssignmentPollPDF(PDFView):
candidate = option.candidate candidate = option.candidate
cell.append(Paragraph("<font name='circlefont' size='15'>%s</font> \ cell.append(Paragraph("<font name='circlefont' size='15'>%s</font> \
<font name='Ubuntu'>%s</font>" % <font name='Ubuntu'>%s</font>" %
(circle, candidate.clean_name), stylesheet['Ballot_option_name'])) (circle, candidate.get_short_name()), stylesheet['Ballot_option_name']))
if candidate.structure_level: if candidate.structure_level:
cell.append(Paragraph( cell.append(Paragraph(
"(%s)" % candidate.structure_level, "(%s)" % candidate.structure_level,

View File

@ -381,7 +381,7 @@ angular.module('OpenSlidesApp.core.site', [
}) })
// Provide generic motion form fields for create and update view // Provide generic customslide form fields for create and update view
.factory('CustomslideFormFieldFactory', [ .factory('CustomslideFormFieldFactory', [
'gettext', 'gettext',
'CKEditorOptions', 'CKEditorOptions',