OpenSlides/openslides/assignments/static/js/assignments/site.js

888 lines
33 KiB
JavaScript

(function () {
'use strict';
angular.module('OpenSlidesApp.assignments.site', [
'OpenSlidesApp.assignments',
'OpenSlidesApp.core.pdf',
'OpenSlidesApp.assignments.pdf',
'OpenSlidesApp.poll.majority'
])
.config([
'mainMenuProvider',
'gettext',
function (mainMenuProvider, gettext) {
mainMenuProvider.register({
'ui_sref': 'assignments.assignment.list',
'img_class': 'pie-chart',
'title': gettext('Elections'),
'weight': 400,
'perm': 'assignments.can_see'
});
}
])
.config([
'SearchProvider',
'gettext',
function (SearchProvider, gettext) {
SearchProvider.register({
'verboseName': gettext('Elections'),
'collectionName': 'assignments/assignment',
'urlDetailState': 'assignments.assignment.detail',
'weight': 400,
});
}
])
.config([
'$stateProvider',
'gettext',
function($stateProvider, gettext) {
$stateProvider
.state('assignments', {
url: '/assignments',
abstract: true,
template: "<ui-view/>",
data: {
title: gettext('Elections'),
basePerm: 'assignments.can_see',
},
})
.state('assignments.assignment', {
abstract: true,
template: "<ui-view/>",
})
.state('assignments.assignment.list', {})
.state('assignments.assignment.detail', {
controller: 'AssignmentDetailCtrl',
resolve: {
assignmentId: ['$stateParams', function($stateParams) {
return $stateParams.id;
}],
}
})
// redirects to assignment detail and opens assignment edit form dialog, uses edit url,
// used by ui-sref links from agenda only
// (from assignment controller use AssignmentForm factory instead to open dialog in front
// of current view without redirect)
.state('assignments.assignment.detail.update', {
onEnter: ['$stateParams', '$state', 'ngDialog',
function($stateParams, $state, ngDialog) {
ngDialog.open({
template: 'static/templates/assignments/assignment-form.html',
controller: 'AssignmentUpdateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: {
assignmentId: function() {
return $stateParams.id;
},
},
preCloseCallback: function() {
$state.go('assignments.assignment.detail', {assignment: $stateParams.id});
return true;
}
});
}
]
});
}
])
// Service for generic assignment form (create and update)
.factory('AssignmentForm', [
'gettextCatalog',
'operator',
'Editor',
'Mediafile',
'Tag',
'Assignment',
'Agenda',
'AgendaTree',
function (gettextCatalog, operator, Editor, Mediafile, Tag, Assignment, Agenda, AgendaTree) {
return {
// ngDialog for assignment form
getDialog: function (assignment) {
return {
template: 'static/templates/assignments/assignment-form.html',
controller: (assignment) ? 'AssignmentUpdateCtrl' : 'AssignmentCreateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: {
assignmentId: function () {return assignment ? assignment.id : void 0;}
},
};
},
// angular-formly fields for assignment form
getFormFields: function (isCreateForm) {
var images = Mediafile.getAllImages();
var formFields = [
{
key: 'title',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Title'),
required: true
}
},
{
key: 'description',
type: 'editor',
templateOptions: {
label: gettextCatalog.getString('Description')
},
data: {
ckeditorOptions: Editor.getOptions(images)
}
},
{
key: 'open_posts',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Number of persons to be elected'),
type: 'number',
min: 1,
required: true
}
},
{
key: 'poll_description_default',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Default comment on the ballot paper')
}
}];
// show as agenda item + parent item
if (isCreateForm) {
formFields.push({
key: 'showAsAgendaItem',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show as agenda item'),
description: gettextCatalog.getString('If deactivated the election appears as internal item on agenda.')
},
hide: !(operator.hasPerms('assignments.can_manage') && operator.hasPerms('agenda.can_manage'))
});
formFields.push({
key: 'agenda_parent_id',
type: 'select-single',
templateOptions: {
label: gettextCatalog.getString('Parent item'),
options: AgendaTree.getFlatTree(Agenda.getAll()),
ngOptions: 'item.id as item.getListViewTitle() for item in to.options | notself : model.agenda_item_id',
placeholder: gettextCatalog.getString('Select a parent item ...')
},
hide: !operator.hasPerms('agenda.can_manage')
});
}
// more (with tags field)
if (Tag.getAll().length > 0) {
formFields.push(
{
key: 'more',
type: 'checkbox',
templateOptions: {
label: gettextCatalog.getString('Show extended fields')
},
hide: !operator.hasPerms('assignments.can_manage')
},
{
template: '<hr class="smallhr">',
hideExpression: '!model.more'
},
{
key: 'tags_id',
type: 'select-multiple',
templateOptions: {
label: gettextCatalog.getString('Tags'),
options: Tag.getAll(),
ngOptions: 'option.id as option.name for option in to.options',
placeholder: gettextCatalog.getString('Select or search a tag ...')
},
hideExpression: '!model.more'
}
);
}
return formFields;
}
};
}
])
// Cache for AssignmentPollDetailCtrl so that users choices are keeped during user actions (e. g. save poll form).
.value('AssignmentPollDetailCtrlCache', {})
// Child controller of AssignmentDetailCtrl for each single poll.
.controller('AssignmentPollDetailCtrl', [
'$scope',
'MajorityMethodChoices',
'Config',
'AssignmentPollDetailCtrlCache',
'AssignmentPoll',
function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache, AssignmentPoll) {
// Define choices.
$scope.methodChoices = MajorityMethodChoices;
// TODO: Get $scope.baseChoices from config_variables.py without copying them.
// Setup empty cache with default values.
if (typeof AssignmentPollDetailCtrlCache[$scope.poll.id] === 'undefined') {
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
method: $scope.config('assignments_poll_default_majority_method'),
};
}
// Fetch users choices from cache.
$scope.method = AssignmentPollDetailCtrlCache[$scope.poll.id].method;
$scope.recalculateMajorities = function (method) {
$scope.method = method;
_.forEach($scope.poll.options, function (option) {
option.majorityReached = option.isReached(method);
});
};
$scope.recalculateMajorities($scope.method);
$scope.saveDescriptionChange = function (poll) {
AssignmentPoll.save(poll);
};
// Save current values to cache on destroy of this controller.
$scope.$on('$destroy', function() {
AssignmentPollDetailCtrlCache[$scope.poll.id] = {
method: $scope.method,
};
});
}
])
.controller('AssignmentListCtrl', [
'$scope',
'ngDialog',
'AssignmentForm',
'Assignment',
'Tag',
'Agenda',
'Projector',
'ProjectionDefault',
'gettextCatalog',
'User',
'osTableFilter',
'osTableSort',
'osTablePagination',
'gettext',
'AssignmentPhases',
'AssignmentPdfExport',
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, Projector,
ProjectionDefault, gettextCatalog, User, osTableFilter, osTableSort, osTablePagination,
gettext, AssignmentPhases, AssignmentPdfExport) {
$scope.$watch(function () {
return Assignment.lastModified();
}, function () {
$scope.assignments = _.orderBy(Assignment.getAll(), ['title']);
});
Tag.bindAll({}, $scope, 'tags');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.phases = AssignmentPhases;
$scope.alert = {};
// Filtering
$scope.filter = osTableFilter.createInstance('AssignmentTableFilter');
if (!$scope.filter.existsStorageEntry()) {
$scope.filter.multiselectFilters = {
tag: [],
phase: [],
};
}
$scope.filter.propertyList = ['title', 'description'];
$scope.filter.propertyFunctionList = [
function (assignment) {
return gettextCatalog.getString($scope.phases[assignment.phase].display_name);
},
];
$scope.filter.propertyDict = {
'assignment_related_users': function (candidate) {
return candidate.user.get_short_name();
},
'tags': function (tag) {
return tag.name;
},
};
$scope.getItemId = {
tag: function (assignment) {return assignment.tags_id;},
phase: function (assignment) {return assignment.phase;},
};
// Sorting
$scope.sort = osTableSort.createInstance('AssignmentTableSort');
if (!$scope.sort.column) {
$scope.sort.column = 'title';
}
$scope.sortOptions = [
{name: 'agenda_item.getItemNumberWithAncestors()',
display_name: gettext('Item')},
{name: 'title',
display_name: gettext('Title')},
{name: 'phase',
display_name: gettext('Phase')},
{name: 'assignment_related_users.length',
display_name: gettext('Number of candidates')},
];
$scope.hasTag = function (assignment, tag) {
return _.indexOf(assignment.tags_id, tag.id) > -1;
};
$scope.toggleTag = function (assignment, tag) {
if ($scope.hasTag(assignment, tag)) {
assignment.tags_id = _.filter(assignment.tags_id, function (tag_id){
return tag_id != tag.id;
});
} else {
assignment.tags_id.push(tag.id);
}
Assignment.save(assignment);
};
// Pagination
$scope.pagination = osTablePagination.createInstance('AssignmentTablePagination');
// update phase
$scope.updatePhase = function (assignment, phase_id) {
assignment.phase = phase_id;
Assignment.save(assignment);
};
// open new/edit dialog
$scope.openDialog = function (assignment) {
ngDialog.open(AssignmentForm.getDialog(assignment));
};
// *** select mode functions ***
$scope.isSelectMode = false;
// check all checkboxes
$scope.checkAll = function () {
$scope.selectedAll = !$scope.selectedAll;
angular.forEach($scope.assignments, function (assignment) {
assignment.selected = $scope.selectedAll;
});
};
// uncheck all checkboxes if isSelectMode is closed
$scope.uncheckAll = function () {
if (!$scope.isSelectMode) {
$scope.selectedAll = false;
angular.forEach($scope.assignments, function (assignment) {
assignment.selected = false;
});
}
};
// delete all selected assignments
$scope.deleteMultiple = function () {
angular.forEach($scope.assignments, function (assignment) {
if (assignment.selected)
Assignment.destroy(assignment.id);
});
$scope.isSelectMode = false;
$scope.uncheckAll();
};
// delete single assignment
$scope.delete = function (assignment) {
Assignment.destroy(assignment.id);
};
// create the PDF List
$scope.pdfExport = function () {
AssignmentPdfExport.export($scope.assignmentsFiltered);
};
}
])
.controller('AssignmentDetailCtrl', [
'$scope',
'$http',
'$filter',
'$timeout',
'filterFilter',
'gettext',
'ngDialog',
'AssignmentForm',
'operator',
'Assignment',
'User',
'assignmentId',
'Projector',
'ProjectionDefault',
'gettextCatalog',
'AssignmentPhases',
'AssignmentPdfExport',
'WebpageTitle',
'ErrorMessage',
function($scope, $http, $filter, $timeout, filterFilter, gettext, ngDialog, AssignmentForm, operator,
Assignment, User, assignmentId, Projector, ProjectionDefault, gettextCatalog, AssignmentPhases,
AssignmentPdfExport, WebpageTitle, ErrorMessage) {
User.bindAll({}, $scope, 'users');
var assignment = Assignment.get(assignmentId);
Assignment.loadRelations(assignment, 'agenda_item');
// This flag is for setting 'activeTab' to recently added (last) ballot tab.
// Set this flag, if ballots are added/removed. When the next autoupdate comes
// in, the tabset will be updated.
var updateBallotTabsFlag = true;
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.$watch(function () {
return Assignment.lastModified(assignmentId);
}, function () {
// setup sorting of candidates
$scope.relatedUsersSorted = $filter('orderBy')(assignment.assignment_related_users, 'weight');
$scope.assignment = Assignment.get(assignment.id);
if (updateBallotTabsFlag) {
$scope.activeTab = $scope.assignment.polls.length - 1;
updateBallotTabsFlag = false;
}
WebpageTitle.updateTitle(gettextCatalog.getString('Election') + ' ' + $scope.assignment.title);
});
$scope.candidateSelectBox = {};
$scope.phases = AssignmentPhases;
$scope.alert = {};
// open edit dialog
$scope.openDialog = function () {
ngDialog.open(AssignmentForm.getDialog($scope.assignment));
};
// add (nominate) candidate
$scope.addCandidate = function (userId) {
$http.post('/rest/assignments/assignment/' + assignmentId + '/candidature_other/', {'user': userId})
.then(function (success){
$scope.alert.show = false;
$scope.candidateSelectBox = {};
}, function (error){
$scope.alert = ErrorMessage.forAlert(error);
$scope.candidateSelectBox = {};
});
};
// remove candidate
$scope.removeCandidate = function (userId) {
$http.delete('/rest/assignments/assignment/' + assignmentId + '/candidature_other/',
{headers: {'Content-Type': 'application/json'},
data: JSON.stringify({user: userId})})
.then(function (success) {},
function (error) {
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
// add me (nominate self as candidate)
$scope.addMe = function () {
$http.post('/rest/assignments/assignment/' + assignmentId + '/candidature_self/', {}).then(
function (success) {
$scope.alert.show = false;
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
// remove me (withdraw own candidature)
$scope.removeMe = function () {
$http.delete('/rest/assignments/assignment/' + assignmentId + '/candidature_self/').then(
function (success) {
$scope.alert.show = false;
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
// check if current user is already a candidate (elected==false)
$scope.isCandidate = function () {
var check = $scope.assignment.assignment_related_users.map(function(candidate) {
if (!candidate.elected) {
return candidate.user_id;
}
}).indexOf(operator.user.id);
if (check > -1) {
return true;
} 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/' + assignmentId + '/sort_related_users/',
{related_users: sortedCandidates}
);
}
};
// update phase
$scope.updatePhase = function (phase_id) {
$scope.assignment.phase = phase_id;
Assignment.save($scope.assignment);
};
// create new ballot
$scope.createBallot = function () {
$http.post('/rest/assignments/assignment/' + assignmentId + '/create_poll/').then(
function (success) {
$scope.alert.show = false;
if (assignment.phase === 0) {
$scope.updatePhase(1);
}
updateBallotTabsFlag = true;
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
// delete ballot
$scope.deleteBallot = function (poll) {
poll.DSDestroy().then(
function (success) {
$scope.activeTab = $scope.activeTab - 1;
updateBallotTabsFlag = true;
}
);
};
// edit poll dialog
$scope.editPollDialog = function (poll, ballot) {
ngDialog.open({
template: 'static/templates/assignments/assignmentpoll-form.html',
controller: 'AssignmentPollUpdateCtrl',
className: 'ngdialog-theme-default',
closeByEscape: false,
closeByDocument: false,
resolve: {
assignmentpollId: function () {return poll.id;},
ballot: function () {return ballot;},
}
});
};
// publish ballot
$scope.togglePublishBallot = function (poll) {
poll.DSUpdate({
assignment_id: assignmentId,
published: !poll.published,
})
.then(function (success) {
$scope.alert.show = false;
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
});
};
// mark candidate as (not) elected
$scope.markElected = function (user, reverse) {
if (reverse) {
$http.delete(
'/rest/assignments/assignment/' + assignmentId + '/mark_elected/', {
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({user: user})
});
} else {
$http.post('/rest/assignments/assignment/' + assignmentId + '/mark_elected/', {'user': user});
}
};
// Creates the document as pdf
$scope.pdfExport = function() {
AssignmentPdfExport.export($scope.assignment, true);
};
// Creates the ballotpaper as pdf
$scope.ballotpaperExport = function(pollId) {
AssignmentPdfExport.createBallotPdf($scope.assignment, pollId);
};
// Just mark some vote value strings for translation.
gettext('Yes');
gettext('No');
gettext('Abstain');
}
])
.controller('AssignmentCreateCtrl', [
'$scope',
'$state',
'Assignment',
'AssignmentForm',
'Agenda',
'ErrorMessage',
function($scope, $state, Assignment, AssignmentForm, Agenda, ErrorMessage) {
$scope.model = {};
// set default value for open posts form field
$scope.model.open_posts = 1;
// get all form fields
$scope.formFields = AssignmentForm.getFormFields(true);
// save assignment
$scope.save = function(assignment, gotoDetailView) {
assignment.agenda_type = assignment.showAsAgendaItem ? 1 : 2;
// The attribute assignment.agenda_parent_id is set by the form, see form definition.
Assignment.create(assignment).then(
function (success) {
if (gotoDetailView) {
$state.go('assignments.assignment.detail', {id: success.id});
}
$scope.closeThisDialog();
},
function (error) {
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
}
])
.controller('AssignmentUpdateCtrl', [
'$scope',
'$state',
'Assignment',
'AssignmentForm',
'Agenda',
'assignmentId',
'ErrorMessage',
function($scope, $state, Assignment, AssignmentForm, Agenda, assignmentId, ErrorMessage) {
var assignment = Assignment.get(assignmentId);
$scope.alert = {};
// set initial values for form model by create deep copy of assignment object
// so list/detail view is not updated while editing
$scope.model = angular.copy(assignment);
// get all form fields
$scope.formFields = AssignmentForm.getFormFields();
// save assignment
$scope.save = function (assignment, gotoDetailView) {
// inject the changed assignment (copy) object back into DS store
Assignment.inject(assignment);
// save changed assignment object on server
Assignment.save(assignment).then(
function(success) {
if (gotoDetailView) {
$state.go('assignments.assignment.detail', {id: success.id});
}
$scope.closeThisDialog();
},
function (error) {
// save error: revert all changes by restore
// (refresh) original assignment object from server
Assignment.refresh(assignment);
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
}
])
.controller('AssignmentPollUpdateCtrl', [
'$scope',
'$filter',
'gettextCatalog',
'AssignmentPoll',
'assignmentpollId',
'ballot',
'ErrorMessage',
function($scope, $filter, gettextCatalog, AssignmentPoll, assignmentpollId, ballot, ErrorMessage) {
// set initial values for form model by create deep copy of assignmentpoll object
// so detail view is not updated while editing poll
var assignmentpoll = angular.copy(AssignmentPoll.get(assignmentpollId));
$scope.model = assignmentpoll;
$scope.ballot = ballot;
$scope.formFields = [];
$scope.alert = {};
// add dynamic form fields
var options = $filter('orderBy')(assignmentpoll.options, 'weight');
options.forEach(function(option) {
var defaultValue;
if (assignmentpoll.pollmethod == 'yna' || assignmentpoll.pollmethod == 'yn') {
defaultValue = {};
_.forEach(option.votes, function (vote) {
defaultValue[vote.value.toLowerCase()] = vote.weight;
});
$scope.formFields.push(
{
noFormControl: true,
template: '<strong>' + option.candidate.get_full_name() + '</strong>'
},
{
key: 'yes_' + option.candidate_id,
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Yes'),
type: 'number',
required: true
},
defaultValue: defaultValue.yes
},
{
key: 'no_' + option.candidate_id,
type: 'input',
templateOptions: {
label: gettextCatalog.getString('No'),
type: 'number',
required: true
},
defaultValue: defaultValue.no
}
);
if (assignmentpoll.pollmethod == 'yna'){
$scope.formFields.push(
{
key:'abstain_' + option.candidate_id,
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Abstain'),
type: 'number',
required: true
},
defaultValue: defaultValue.abstain
});
}
} else { // votes method
if (option.votes.length) {
defaultValue = option.votes[0].weight;
}
$scope.formFields.push(
{
key: 'vote_' + option.candidate_id,
type: 'input',
templateOptions: {
label: option.candidate.get_full_name(),
type: 'number',
required: true
},
defaultValue: defaultValue
});
}
});
// add general form fields
$scope.formFields.push(
{
key: 'votesvalid',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Valid ballots'),
type: 'number'
}
},
{
key: 'votesinvalid',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Invalid ballots'),
type: 'number'
}
},
{
key: 'votescast',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Casted ballots'),
type: 'number'
}
}
);
// save assignmentpoll
$scope.save = function (poll) {
var votes = [];
if (assignmentpoll.pollmethod == 'yna') {
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]
});
});
} else if (assignmentpoll.pollmethod == 'yn') {
assignmentpoll.options.forEach(function(option) {
votes.push({
"Yes": poll['yes_' + option.candidate_id],
"No": poll['no_' + option.candidate_id]
});
});
} else {
assignmentpoll.options.forEach(function(option) {
votes.push({
"Votes": poll['vote_' + option.candidate_id],
});
});
}
// save change poll object on server
poll.DSUpdate({
assignment_id: poll.assignment_id,
votes: votes,
votesvalid: poll.votesvalid,
votesinvalid: poll.votesinvalid,
votescast: poll.votescast
})
.then(function(success) {
$scope.alert.show = false;
$scope.closeThisDialog();
}, function (error) {
$scope.alert = ErrorMessage.forAlert(error);
});
};
}
])
//mark all assignment config strings for translation with Javascript
.config([
'gettext',
function (gettext) {
gettext('Election method');
gettext('Automatic assign of method');
gettext('Always one option per candidate');
gettext('Always Yes-No-Abstain per candidate');
gettext('Always Yes/No per candidate');
gettext('Elections');
gettext('Ballot and ballot papers');
gettext('The 100-%-base of an election result consists of');
gettext('For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base ' +
'depends on the election method: If there is only one option per candidate, ' +
'the sum of all votes of all candidates is 100 %. Otherwise for each ' +
'candidate the sum of all votes is 100 %.');
gettext('Yes/No/Abstain per candidate');
gettext('Yes/No per candidate');
gettext('All valid ballots');
gettext('All casted ballots');
gettext('Disabled (no percents)');
gettext('Number of ballot papers (selection)');
gettext('Number of all delegates');
gettext('Number of all participants');
gettext('Use the following custom number');
gettext('Custom number of ballot papers');
gettext('Required majority');
gettext('Default method to check whether a candidate has reached the required majority.');
gettext('Simple majority');
gettext('Two-thirds majority');
gettext('Three-quarters majority');
gettext('Disabled');
gettext('Title for PDF document (all elections)');
gettext('Preamble text for PDF document (all elections)');
//other translations
gettext('Searching for candidates');
gettext('Voting');
gettext('Finished');
}
]);
}());