(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([ '$stateProvider', 'gettext', function($stateProvider, gettext) { $stateProvider .state('assignments', { url: '/assignments', abstract: true, template: "", data: { title: gettext('Elections'), }, }) .state('assignments.assignment', { abstract: true, template: "", }) .state('assignments.assignment.list', { resolve: { assignments: function(Assignment) { return Assignment.findAll(); }, tags: function(Tag) { return Tag.findAll(); }, items: function(Agenda) { return Agenda.findAll().catch( function() { return null; } ); }, phases: function(Assignment) { return Assignment.getPhases(); } } }) .state('assignments.assignment.detail', { controller: 'AssignmentDetailCtrl', resolve: { assignment: function(Assignment, $stateParams) { return Assignment.find($stateParams.id).then(function(assignment) { return assignment; }); }, users: function(User) { return User.findAll(); }, tags: function(Tag) { return Tag.findAll(); }, items: function(Agenda) { return Agenda.findAll().catch( function() { return null; } ); }, phases: function(Assignment) { return Assignment.getPhases(); } } }) // 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', 'Assignment', function($stateParams, $state, ngDialog, Assignment) { ngDialog.open({ template: 'static/templates/assignments/assignment-form.html', controller: 'AssignmentUpdateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { assignment: function() { return Assignment.find($stateParams.id).then(function(assignment) { return Assignment.loadRelations(assignment, 'agenda_item'); }); }, items: function(Agenda) { return Agenda.findAll().catch( function() { return null; }); }, }, preCloseCallback: function() { $state.go('assignments.assignment.detail', {assignment: $stateParams.id}); return true; } }); } ] }); } ]) // Service for generic assignment form (create and update) .factory('AssignmentForm', [ 'gettextCatalog', 'operator', 'Tag', 'Assignment', 'Agenda', 'AgendaTree', function (gettextCatalog, operator, Tag, Assignment, Agenda, AgendaTree) { return { // ngDialog for assignment form getDialog: function (assignment) { var resolve = {}; if (assignment) { resolve.assignment = function () { return assignment; }; resolve.agenda_item = function () { return Assignment.loadRelations(assignment, 'agenda_item'); }; } resolve.items = function() { return Agenda.getAll(); }; return { template: 'static/templates/assignments/assignment-form.html', controller: (assignment) ? 'AssignmentUpdateCtrl' : 'AssignmentCreateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: resolve }; }, // angular-formly fields for assignment form getFormFields: function (isCreateForm) { var formFields = [ { key: 'title', type: 'input', templateOptions: { label: gettextCatalog.getString('Title'), required: true } }, { key: 'description', type: 'textarea', templateOptions: { label: gettextCatalog.getString('Description') } }, { key: 'open_posts', type: 'input', templateOptions: { label: gettextCatalog.getString('Number of persons to be elected'), type: 'number', required: true } }, { key: 'poll_description_default', type: 'input', templateOptions: { label: gettextCatalog.getString('Default comment on the ballot paper') } }, { 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') }]; // parent item if (isCreateForm) { formFields.push({ key: 'agenda_parent_item_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: '
', 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', function ($scope, MajorityMethodChoices, Config, AssignmentPollDetailCtrlCache) { // 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); // 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', 'phases', 'Projector', 'ProjectionDefault', 'gettextCatalog', 'AssignmentContentProvider', 'AssignmentCatalogContentProvider', 'PdfMakeDocumentProvider', 'User', 'osTableFilter', 'osTableSort', 'gettext', function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, phases, Projector, ProjectionDefault, gettextCatalog, AssignmentContentProvider, AssignmentCatalogContentProvider, PdfMakeDocumentProvider, User, osTableFilter, osTableSort, gettext) { Assignment.bindAll({}, $scope, 'assignments'); 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 = phases; $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(); $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.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); }; // 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 () { 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.makePDF_assignmentList = function () { User.findAll().then( function(users) { var filename = gettextCatalog.getString("Elections") + ".pdf"; var assignmentContentProviderArray = []; //convert the filtered assignments to content providers angular.forEach($scope.assignmentsFiltered, function(assignment) { assignmentContentProviderArray.push(AssignmentContentProvider.createInstance(assignment)); }); var assignmentCatalogContentProvider = AssignmentCatalogContentProvider.createInstance(assignmentContentProviderArray); var documentProvider = PdfMakeDocumentProvider.createInstance(assignmentCatalogContentProvider); pdfMake.createPdf(documentProvider.getDocument()).download(filename); }); }; } ]) .controller('AssignmentDetailCtrl', [ '$scope', '$http', 'filterFilter', 'gettext', 'ngDialog', 'AssignmentForm', 'operator', 'Assignment', 'User', 'assignment', 'phases', 'Projector', 'ProjectionDefault', 'AssignmentContentProvider', 'BallotContentProvider', 'PdfMakeDocumentProvider', 'PdfMakeBallotPaperProvider', 'gettextCatalog', function($scope, $http, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment, User, assignment, phases, Projector, ProjectionDefault, AssignmentContentProvider, BallotContentProvider, PdfMakeDocumentProvider, PdfMakeBallotPaperProvider, gettextCatalog) { User.bindAll({}, $scope, 'users'); Assignment.bindOne(assignment.id, $scope, 'assignment'); Assignment.loadRelations(assignment, 'agenda_item'); $scope.$watch(function () { return Projector.lastModified(); }, function () { var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0]; if (projectiondefault) { $scope.defaultProjectorId = projectiondefault.projector_id; } }); $scope.candidateSelectBox = {}; $scope.phases = phases; $scope.alert = {}; $scope.activeTab = 0; // open edit dialog $scope.openDialog = function (assignment) { ngDialog.open(AssignmentForm.getDialog(assignment)); }; // 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 $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 }; }); }; // 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 (elected==false) $scope.isCandidate = function () { var check = 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; }; // update phase $scope.updatePhase = function (phase_id) { assignment.phase = phase_id; Assignment.save(assignment); }; // create new ballot $scope.createBallot = function () { $scope.activeTab = 0; $http.post('/rest/assignments/assignment/' + assignment.id + '/create_poll/') .success(function(data){ $scope.alert.show = false; if (assignment.phase === 0) { $scope.updatePhase(1); } }) .error(function(data){ $scope.alert = { type: 'danger', msg: data.detail, show: true }; }); }; // delete ballot $scope.deleteBallot = function (poll) { poll.DSDestroy(); }; // 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: { assignmentpoll: function (AssignmentPoll) { return AssignmentPoll.find(poll.id); }, ballot: function () { return ballot; } } }); }; // publish ballot $scope.togglePublishBallot = function (poll) { poll.DSUpdate({ assignment_id: assignment.id, published: !poll.published, }) .then(function(success) { $scope.alert.show = false; }) .catch(function(error) { var message = ''; for (var e in error.data) { message += e + ': ' + error.data[e] + ' '; } $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}) .then(function(success) { var elected = filterFilter(assignment.assignment_related_users,{elected: true}); // Set phase to 'finished' if there are enough (= number of open posts) elected users. // Note: The array 'elected' does NOT contains the candidate who is just marked as elected. // So add 1 to length to get the real number of all elected users. if (elected.length + 1 == assignment.open_posts ) { $scope.updatePhase(2); } }); } }; //creates the document as pdf $scope.makePDF_singleAssignment = function() { var filename = gettextCatalog.getString("Election") + "_" + $scope.assignment.title + ".pdf"; var assignmentContentProvider = AssignmentContentProvider.createInstance(assignment); var documentProvider = PdfMakeDocumentProvider.createInstance(assignmentContentProvider); pdfMake.createPdf(documentProvider.getDocument()).download(filename); }; //creates the ballotpaper as pdf $scope.makePDF_assignmentpoll = function(pollID) { var thePoll; var pollNumber; angular.forEach(assignment.polls, function(poll, pollIndex) { if (poll.id == pollID) { thePoll = poll; pollNumber = pollIndex+1; } }); var filename = gettextCatalog.getString("Ballot") + "_" + pollNumber + "_" + $scope.assignment.title + ".pdf"; var ballotContentProvider = BallotContentProvider.createInstance($scope, thePoll, pollNumber); var documentProvider = PdfMakeBallotPaperProvider.createInstance(ballotContentProvider); pdfMake.createPdf(documentProvider.getDocument()).download(filename); }; // Just mark some vote value strings for translation. gettext('Yes'); gettext('No'); gettext('Abstain'); } ]) .controller('AssignmentCreateCtrl', [ '$scope', 'Assignment', 'AssignmentForm', 'Agenda', 'AgendaUpdate', function($scope, Assignment, AssignmentForm, Agenda, AgendaUpdate) { $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) { Assignment.create(assignment).then( function(success) { // type: Value 1 means a non hidden agenda item, value 2 means a hidden agenda item, // see openslides.agenda.models.Item.ITEM_TYPE. var changes = [{key: 'type', value: (assignment.showAsAgendaItem ? 1 : 2)}, {key: 'parent_id', value: assignment.agenda_parent_item_id}]; AgendaUpdate.saveChanges(success.agenda_item_id,changes); $scope.closeThisDialog(); } ); }; } ]) .controller('AssignmentUpdateCtrl', [ '$scope', 'Assignment', 'AssignmentForm', 'Agenda', 'AgendaUpdate', 'assignment', function($scope, Assignment, AssignmentForm, Agenda, AgendaUpdate, assignment) { $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(); var agenda_item = Agenda.get(assignment.agenda_item_id); for (var i = 0; i < $scope.formFields.length; i++) { if ($scope.formFields[i].key == "showAsAgendaItem") { // get state from agenda item (hidden/internal or agenda item) $scope.formFields[i].defaultValue = !assignment.agenda_item.is_hidden; } else if($scope.formFields[i].key == 'agenda_parent_item_id') { $scope.formFields[i].defaultValue = agenda_item.parent_id; } } // save assignment $scope.save = function (assignment) { // inject the changed assignment (copy) object back into DS store Assignment.inject(assignment); // save change assignment object on server Assignment.save(assignment).then( function(success) { var changes = [{key: 'type', value: (assignment.showAsAgendaItem ? 1 : 2)}, {key: 'parent_id', value: assignment.agenda_parent_item_id}]; AgendaUpdate.saveChanges(success.agenda_item_id,changes); $scope.closeThisDialog(); }, function (error) { // save error: revert all changes by restore // (refresh) original assignment object from server Assignment.refresh(assignment); var message = ''; for (var e in error.data) { message += e + ': ' + error.data[e] + ' '; } $scope.alert = {type: 'danger', msg: message, show: true}; } ); }; } ]) .controller('AssignmentPollUpdateCtrl', [ '$scope', 'gettextCatalog', 'AssignmentPoll', 'assignmentpoll', 'ballot', function($scope, gettextCatalog, AssignmentPoll, assignmentpoll, ballot) { // set initial values for form model by create deep copy of assignmentpoll object // so detail view is not updated while editing poll $scope.model = angular.copy(assignmentpoll); $scope.ballot = ballot; $scope.formFields = []; $scope.alert = {}; // add dynamic form fields assignmentpoll.options.forEach(function(option) { var defaultValue; if (assignmentpoll.pollmethod == 'yna' || assignmentpoll.pollmethod == 'yn') { if (assignmentpoll.pollmethod == 'yna') { defaultValue = { 'yes': '', 'no': '', 'abstain': '' }; } else { defaultValue = { 'yes': '', 'no': '' }; } if (option.votes.length) { defaultValue.yes = option.votes[0].weight; defaultValue.no = option.votes[1].weight; if (assignmentpoll.pollmethod == 'yna'){ defaultValue.abstain = option.votes[2].weight; } } $scope.formFields.push( { noFormControl: true, template: '' + option.candidate.get_full_name() + '' }, { 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 { 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' } }, // TODO: update description in separat request // (without vote result values) { key: 'description', type: 'input', templateOptions: { label: gettextCatalog.getString('Comment on the ballot paper') } } ); // 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(); }) .catch(function(error) { var message = ''; for (var e in error.data) { message += e + ': ' + error.data[e] + ' '; } $scope.alert = { type: 'danger', msg: message, show: true }; }); }; } ]) //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'); } ]); }());