(function () { 'use strict'; angular.module('OpenSlidesApp.motions.site', [ 'OpenSlidesApp.motions', 'OpenSlidesApp.motions.motionservices', 'OpenSlidesApp.poll.majority', 'OpenSlidesApp.core.pdf', 'OpenSlidesApp.motions.docx', 'OpenSlidesApp.motions.pdf', 'OpenSlidesApp.motions.csv', 'OpenSlidesApp.motions.workflow', ]) .config([ 'mainMenuProvider', 'gettext', function (mainMenuProvider, gettext) { mainMenuProvider.register({ 'ui_sref': 'motions.motion.list', 'img_class': 'file-text', 'title': gettext('Motions'), 'weight': 300, 'perm': 'motions.can_see', }); } ]) .config([ 'SearchProvider', 'gettext', function (SearchProvider, gettext) { SearchProvider.register({ 'verboseName': gettext('Motions'), 'collectionName': 'motions/motion', 'urlDetailState': 'motions.motion.detail', 'weight': 300, }); } ]) .config([ '$stateProvider', 'gettext', function($stateProvider, gettext) { $stateProvider .state('motions', { url: '/motions', abstract: true, template: "", data: { title: gettext('Motions'), basePerm: 'motions.can_see', }, }) .state('motions.motion', { abstract: true, template: "", }) .state('motions.motion.list', {}) .state('motions.motion.detail', { resolve: { motionId: ['$stateParams', function($stateParams) { return $stateParams.id; }], } }) // redirects to motion detail and opens motion edit form dialog, uses edit url, // used by ui-sref links from agenda only // (from motion controller use MotionForm factory instead to open dialog in front of // current view without redirect) .state('motions.motion.detail.update', { onEnter: ['$stateParams', '$state', 'ngDialog', 'Motion', function($stateParams, $state, ngDialog, Motion) { ngDialog.open({ template: 'static/templates/motions/motion-form.html', controller: 'MotionUpdateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { motionId: function () {return $stateParams.id;}, }, preCloseCallback: function () { $state.go('motions.motion.detail', {motion: $stateParams.id}); return true; } }); } ] }) .state('motions.motion.submitters', { url: '/submitters/{id:int}', controller: 'MotionSubmitterCtrl', resolve: { motionId: ['$stateParams', function($stateParams) { return $stateParams.id; }], }, data: { title: gettext('Submitters'), basePerm: 'motions.can_manage', }, }) .state('motions.motion.amendment-list', { url: '/{id:int}/amendments', controller: 'MotionAmendmentListStateCtrl', params: { motionId: null, }, resolve: { motionId: ['$stateParams', function($stateParams) { return $stateParams.id; }], } }) .state('motions.motion.allamendments', { url: '/amendments', templateUrl: 'static/templates/motions/motion-amendment-list.html', controller: 'MotionAmendmentListStateCtrl', resolve: { motionId: function() { return void 0; }, } }) .state('motions.motion.import', { url: '/import', controller: 'MotionImportCtrl', }) // categories .state('motions.category', { url: '/category', abstract: true, template: "", data: { title: gettext('Categories'), }, }) .state('motions.category.list', {}) .state('motions.category.sort', { url: '/sort/{id}', controller: 'CategorySortCtrl', templateUrl: 'static/templates/motions/category-sort.html', resolve: { categoryId: ['$stateParams', function($stateParams) { return $stateParams.id; }], }, }) // MotionBlock .state('motions.motionBlock', { url: '/blocks', abstract: true, template: '', data: { title: gettext('Motion blocks'), }, }) .state('motions.motionBlock.list', {}) .state('motions.motionBlock.detail', { resolve: { motionBlockId: ['$stateParams', function($stateParams) { return $stateParams.id; }], } }) // redirects to motionBlock detail and opens motionBlock edit form dialog, uses edit url, // used by ui-sref links from agenda only // (from motionBlock controller use MotionBlockForm factory instead to open dialog in front // of current view without redirect) .state('motions.motionBlock.detail.update', { onEnter: ['$stateParams', '$state', 'ngDialog', function($stateParams, $state, ngDialog) { ngDialog.open({ template: 'static/templates/motions/motion-block-form.html', controller: 'MotionBlockUpdateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { motionBlockId: function () { return $stateParams.id; } }, preCloseCallback: function() { $state.go('motions.motionBlock.detail', {motionBlock: $stateParams.id}); return true; } }); } ], }) // Workflows and states .state('motions.workflow', { url: '/workflow', abstract: true, template: '', data: { title: gettext('Workflows'), basePerm: 'motions.can_manage', }, }) .state('motions.workflow.list', {}) .state('motions.workflow.detail', { resolve: { workflowId: ['$stateParams', function($stateParams) { return $stateParams.id; }], } }); } ]) .factory('ChangeRecommendationTitleForm', [ 'gettextCatalog', 'Editor', 'Config', function(gettextCatalog) { return { // ngDialog for motion form getCreateDialog: function (motion, version) { return { template: 'static/templates/motions/change-recommendation-form.html', controller: 'ChangeRecommendationTitleCreateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { motion: function() { return motion; }, version: function() { return version; } } }; }, getEditDialog: function(change) { return { template: 'static/templates/motions/change-recommendation-form.html', controller: 'ChangeRecommendationTitleUpdateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { change: function() { return change; } } }; }, // angular-formly fields for motion form getFormFields: function () { return [ { key: 'identifier', type: 'input', templateOptions: { label: gettextCatalog.getString('Identifier') }, hide: true }, { key: 'motion_version_id', type: 'input', templateOptions: { label: gettextCatalog.getString('Motion') }, hide: true }, { key: 'text', type: 'input', templateOptions: { label: gettextCatalog.getString('New title'), required: false } } ]; } }; } ]) .factory('ChangeRecommendationTextForm', [ 'gettextCatalog', 'Editor', 'Config', function(gettextCatalog, Editor) { return { // ngDialog for motion form getCreateDialog: function (motion, version, lineFrom, lineTo) { return { template: 'static/templates/motions/change-recommendation-form.html', controller: 'ChangeRecommendationTextCreateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { motion: function() { return motion; }, version: function() { return version; }, lineFrom: function() { return lineFrom; }, lineTo: function() { return lineTo; } } }; }, getEditDialog: function(change) { return { template: 'static/templates/motions/change-recommendation-form.html', controller: 'ChangeRecommendationTextUpdateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { change: function() { return change; } } }; }, // angular-formly fields for motion form getFormFields: function (line_from, line_to) { return [ { key: 'identifier', type: 'input', templateOptions: { label: gettextCatalog.getString('Identifier') }, hide: true }, { key: 'motion_version_id', type: 'input', templateOptions: { label: gettextCatalog.getString('Motion') }, hide: true }, { key: 'line_from', type: 'input', templateOptions: { label: gettextCatalog.getString('From Line') }, hide: true }, { key: 'line_to', type: 'input', templateOptions: { label: gettextCatalog.getString('To Line') }, hide: true }, { key: 'type', type: 'radio-buttons', templateOptions: { label: 'Type', options: [ {name: gettextCatalog.getString('Replacement'), value: 0}, {name: gettextCatalog.getString('Insertion'), value: 1}, {name: gettextCatalog.getString('Deletion'), value: 2}, {name: gettextCatalog.getString('Other'), value: 3}, ] } }, { key: 'other_description', type: 'input', templateOptions: { label: gettextCatalog.getString('Description'), }, hideExpression: "model.type !== 3", }, { key: 'text', type: 'editor', templateOptions: { label: ( line_from == line_to - 1 ? gettextCatalog.getString('Text in line %from%').replace(/%from%/, line_from) : gettextCatalog.getString('Text from line %from% to %to%') .replace(/%from%/, line_from).replace(/%to%/, line_to - 1) ), required: false }, data: { ckeditorOptions: Editor.getOptions() } } ]; } }; } ]) // Service for choosing the paragraph of a given motion that is to be amended .factory('AmendmentParagraphChooseForm', [ function () { return { // ngDialog for motion form getDialog: function (motion, successCb) { return { template: 'static/templates/motions/amendment-paragraph-choose-form.html', controller: 'AmendmentParagraphChooseCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { motion: function () { return motion; }, successCb: function() { return successCb; }, } }; } }; } ]) // Service for generic motion form (create and update) .factory('MotionForm', [ '$filter', 'gettextCatalog', 'operator', 'Editor', 'MotionComment', 'Category', 'Config', 'Mediafile', 'MotionBlock', 'Tag', 'User', 'Workflow', 'Agenda', 'AgendaTree', 'ShowAsAgendaItemField', function ($filter, gettextCatalog, operator, Editor, MotionComment, Category, Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree, ShowAsAgendaItemField) { return { // ngDialog for motion form // If motion is given and not null, we're editing an already existing motion // If parentMotion is give, we're dealing with an amendment // If paragraphNo is given as well, the amendment is paragraph-based // If paragraphTextPre is given, we're creating a modified version of another paragraph-based amendment getDialog: function (motion, parentMotion, paragraphNo, paragraphTextPre) { return { template: 'static/templates/motions/motion-form.html', controller: motion ? 'MotionUpdateCtrl' : 'MotionCreateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { motionId: function () {return motion ? motion.id : void 0;}, parentMotion: function () {return parentMotion;}, paragraphNo: function () {return paragraphNo;}, paragraphTextPre: function () {return paragraphTextPre;} } }; }, // angular-formly fields for motion form getFormFields: function (isCreateForm, isParagraphBasedAmendment) { if (!isParagraphBasedAmendment) { // catch null and undefined. Angular formy doesn't like this. isParagraphBasedAmendment = false; } var workflows = Workflow.getAll(); var images = Mediafile.getAllImages(); var formFields = []; formFields.push({ key: 'identifier', type: 'input', templateOptions: { label: gettextCatalog.getString('Identifier') }, hide: true }); if (isCreateForm) { formFields.push({ key: 'submitters_id', type: 'select-multiple', templateOptions: { label: gettextCatalog.getString('Submitters'), options: User.getAll(), ngOptions: 'option.id as option.full_name for option in to.options', placeholder: gettextCatalog.getString('Select or search a submitter ...'), }, hide: !operator.hasPerms('motions.can_manage') }); } formFields = formFields.concat([ { key: 'title', type: 'input', templateOptions: { label: gettextCatalog.getString('Title'), required: true }, hide: isParagraphBasedAmendment && isCreateForm }, { template: '

' + Config.translate(Config.get('motions_preamble').value) + '

' }, { key: 'text', type: 'editor', templateOptions: { label: gettextCatalog.getString('Text'), required: !isParagraphBasedAmendment // Deleting the whole paragraph in an amendment should be possible }, data: { ckeditorOptions: Editor.getOptions() } }, { key: 'reason', type: 'editor', templateOptions: { label: gettextCatalog.getString('Reason'), }, data: { ckeditorOptions: Editor.getOptions() } }, { key: 'disable_versioning', type: 'checkbox', templateOptions: { label: gettextCatalog.getString('Trivial change'), description: gettextCatalog.getString("Don't create a new version.") }, hide: true } ]); // show as agenda item + parent item if (isCreateForm) { formFields.push(ShowAsAgendaItemField('motions.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') }); } // motion comments formFields = formFields.concat(MotionComment.getFormFields()); // more formFields.push( { key: 'more', type: 'checkbox', templateOptions: { label: gettextCatalog.getString('Show extended fields') }, hide: !operator.hasPerms('motions.can_manage') }, { template: '
', hideExpression: '!model.more' } ); // attachments if (Mediafile.getAll().length > 0) { formFields.push({ key: 'attachments_id', type: 'select-multiple', templateOptions: { label: gettextCatalog.getString('Attachment'), options: $filter('orderBy')(Mediafile.getAll(), 'title_or_filename'), ngOptions: 'option.id as option.title_or_filename for option in to.options', placeholder: gettextCatalog.getString('Select or search an attachment ...') }, hideExpression: '!model.more' }); } // category if (Category.getAll().length > 0) { formFields.push({ key: 'category_id', type: 'select-single', templateOptions: { label: gettextCatalog.getString('Category'), options: Category.getAll(), ngOptions: 'option.id as option.name for option in to.options', placeholder: gettextCatalog.getString('Select or search a category ...') }, hideExpression: '!model.more' }); } // motion block if (MotionBlock.getAll().length > 0) { formFields.push({ key: 'motion_block_id', type: 'select-single', templateOptions: { label: gettextCatalog.getString('Motion block'), options: MotionBlock.getAll(), ngOptions: 'option.id as option.title for option in to.options', placeholder: gettextCatalog.getString('Select or search a motion block ...') }, hideExpression: '!model.more' }); } // origin formFields.push({ key: 'origin', type: 'input', templateOptions: { label: gettextCatalog.getString('Origin'), }, hideExpression: '!model.more' }); // tags if (Tag.getAll().length > 0) { formFields.push({ 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' }); } // supporters if (Config.get('motions_min_supporters').value > 0) { formFields.push({ key: 'supporters_id', type: 'select-multiple', templateOptions: { label: gettextCatalog.getString('Supporters'), options: User.getAll(), ngOptions: 'option.id as option.full_name for option in to.options', placeholder: gettextCatalog.getString('Select or search a supporter ...') }, hideExpression: '!model.more' }); } // workflows if (workflows.length > 1) { formFields.push({ key: 'workflow_id', type: 'select-single', templateOptions: { label: gettextCatalog.getString('Workflow'), optionsAttr: 'bs-options', options: workflows, ngOptions: 'option.id as option.name | translate for option in to.options', placeholder: gettextCatalog.getString('Select or search a workflow ...') }, hideExpression: '!model.more', }); } return formFields; } }; } ]) .factory('MotionCommentForm', [ 'MotionComment', function (MotionComment) { return { // ngDialog for motion comment form getDialog: function (motion, commentFieldId) { return { template: 'static/templates/motions/motion-comment-form.html', controller: 'MotionCommentCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { motionId: function () {return motion.id;}, commentFieldId: function () {return commentFieldId;}, }, }; }, // angular-formly fields for motion comment form getFormFields: function (commentFieldId) { return [ MotionComment.getFormField(commentFieldId) ]; }, }; } ]) .factory('CategoryForm', [ 'gettextCatalog', function (gettextCatalog) { return { getDialog: function (category) { return { template: 'static/templates/motions/category-form.html', controller: category ? 'CategoryUpdateCtrl' : 'CategoryCreateCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { categoryId: function () {return category ? category.id : void 0;}, }, }; }, getFormFields: function () { return [ { key: 'prefix', type: 'input', templateOptions: { label: gettextCatalog.getString('Prefix') }, }, { key: 'name', type: 'input', templateOptions: { label: gettextCatalog.getString('Name') }, } ]; }, }; } ]) // Provide generic motionpoll form fields for poll update view .factory('MotionPollForm', [ 'gettextCatalog', function (gettextCatalog) { return { getFormFields: function (precision) { var step = Math.pow(10, -precision); return [ { key: 'yes', type: 'input', templateOptions: { label: gettextCatalog.getString('Yes'), type: 'number', step: step, required: true } }, { key: 'no', type: 'input', templateOptions: { label: gettextCatalog.getString('No'), type: 'number', step: step, required: true } }, { key: 'abstain', type: 'input', templateOptions: { label: gettextCatalog.getString('Abstain'), type: 'number', step: step, required: true } }, { key: 'votesvalid', type: 'input', templateOptions: { label: gettextCatalog.getString('Valid votes'), step: step, type: 'number' } }, { key: 'votesinvalid', type: 'input', templateOptions: { label: gettextCatalog.getString('Invalid votes'), step: step, type: 'number' } }, { key: 'votescast', type: 'input', templateOptions: { label: gettextCatalog.getString('Votes cast'), step: step, type: 'number' } }]; } }; } ]) .factory('MotionExportForm', [ 'operator', 'gettextCatalog', 'Config', 'MotionComment', function (operator, gettextCatalog, Config, MotionComment) { var noSpecialCommentsFields = MotionComment.getNoSpecialCommentsFields(); return { getDialog: function (motions, params, singleMotion) { return { template: 'static/templates/motions/motion-export-form.html', controller: 'MotionExportCtrl', className: 'ngdialog-theme-default wide-form', closeByEscape: false, closeByDocument: false, resolve: { motions: function () {return motions;}, params: function () {return params;}, singleMotion: function () {return singleMotion;}, }, }; }, getFormFields: function (singleMotion, motions, formatChangeCallback) { var fields = []; var commentsAvailable = _.keys(noSpecialCommentsFields).length !== 0; var someMotionsHaveAmendments = _.some(motions, function (motion) { return motion.hasAmendments(); }); // if amendments amendments are already included. We owudl have them twice, if the option is enabled. if (Config.get('motions_amendments_main_table').value) { someMotionsHaveAmendments = false; } var getMetaInformationOptions = function (disabled) { if (!disabled) { disabled = {}; } var options = [ {name: gettextCatalog.getString('State'), id: 'state', disabled: disabled.state}, {name: gettextCatalog.getString('Submitters'), id: 'submitters', disabled: disabled.submitters}, {name: gettextCatalog.getString('Voting result'), id: 'votingresult', disabled: disabled.votingResult} ]; if (_.some(motions, function (motion) { return motion.motionBlock; })) { options.push({ name: gettextCatalog.getString('Motion block'), id: 'motionBlock', disabled: disabled.motionBlock, }); } if (_.some(motions, function (motion) { return motion.origin; })) { options.push({ name: gettextCatalog.getString('Origin'), id: 'origin', disabled: disabled.origin, }); } if (Config.get('motions_recommendations_by').value) { options.push({ name: gettextCatalog.getString('Recommendation'), id: 'recommendation', disabled: disabled.recommendation }); } return options; }; if (!singleMotion) { fields = [ { key: 'format', type: 'radio-buttons', templateOptions: { label: gettextCatalog.getString('Format'), options: [ {name: 'PDF', value: 'pdf'}, {name: 'CSV', value: 'csv'}, {name: 'DOCX', value: 'docx'}, ], change: formatChangeCallback, }, } ]; } if (someMotionsHaveAmendments) { fields.push({ key: 'amendments', type: 'radio-buttons', templateOptions: { label: gettextCatalog.getString('Amendments'), options: [ {name: gettextCatalog.getString('Include'), value: true}, {name: gettextCatalog.getString('Exclude'), value: false}, ], }, }); } if (operator.hasPerms('motions.can_manage')) { fields.push.apply(fields, [ { key: 'lineNumberMode', type: 'radio-buttons', templateOptions: { label: gettextCatalog.getString('Line numbering'), options: [ {name: gettextCatalog.getString('None'), value: 'none'}, {name: gettextCatalog.getString('inline'), value: 'inline'}, {name: gettextCatalog.getString('outside'), value: 'outside'}, ], }, hideExpression: "model.format !== 'pdf'", }, { key: 'lineNumberMode', type: 'radio-buttons', templateOptions: { label: gettextCatalog.getString('Line numbering'), options: [ {name: gettextCatalog.getString('None'), value: 'none'}, {name: gettextCatalog.getString('inline'), value: 'inline', disabled: true}, {name: gettextCatalog.getString('outside'), value: 'outside', disabled: true}, ], }, hideExpression: "model.format === 'pdf'", }, { key: 'changeRecommendationMode', type: 'radio-buttons', templateOptions: { label: gettextCatalog.getString('Change recommendations'), options: [ {name: gettextCatalog.getString('Original version'), value: 'original'}, {name: gettextCatalog.getString('Changed version'), value: 'changed'}, {name: gettextCatalog.getString('Diff version'), value: 'diff'}, {name: gettextCatalog.getString('Final version'), value: 'modified_agreed'}, ], }, hideExpression: "model.format !== 'pdf'", }, { key: 'changeRecommendationMode', type: 'radio-buttons', templateOptions: { label: gettextCatalog.getString('Change recommendations'), options: [ {name: gettextCatalog.getString('Original version'), value: 'original'}, {name: gettextCatalog.getString('Changed version'), value: 'changed'}, {name: gettextCatalog.getString('Diff version'), value: 'diff', disabled: true}, {name: gettextCatalog.getString('Final version'), value: 'modified_agreed'}, ], }, hideExpression: "model.format === 'pdf'", }, { key: 'include', type: 'checkbox-buttons', templateOptions: { label: gettextCatalog.getString('Content'), options: [ {name: gettextCatalog.getString('Text'), id: 'text'}, {name: gettextCatalog.getString('Reason'), id: 'reason'}, ], }, }, { key: 'include', type: 'checkbox-buttons', templateOptions: { label: gettextCatalog.getString('Meta information'), options: getMetaInformationOptions(), }, hideExpression: "model.format !== 'pdf'", }, { key: 'include', type: 'checkbox-buttons', templateOptions: { label: gettextCatalog.getString('Meta information'), options: getMetaInformationOptions({votingResult: true}), }, hideExpression: "model.format !== 'csv'", }, ]); if (commentsAvailable) { fields.push({ key: 'includeComments', type: 'checkbox-buttons', templateOptions: { label: gettextCatalog.getString('Comments'), options: _.map(noSpecialCommentsFields, function (field, id) { return { name: gettextCatalog.getString(field.name), id: id, }; }), }, hideExpression: "model.format === 'csv'", }); } } if (!singleMotion) { fields.push({ key: 'pdfFormat', type: 'radio-buttons', templateOptions: { label: gettextCatalog.getString('PDF format'), options: [ {name: gettextCatalog.getString('One PDF'), value: 'pdf'}, {name: gettextCatalog.getString('Multiple PDFs in a zip arcive'), value: 'zip'}, ], }, hideExpression: "model.format !== 'pdf'", }); } return fields; }, }; } ]) .controller('MotionExportCtrl', [ '$scope', 'Config', 'MotionExportForm', 'MotionPdfExport', 'MotionCsvExport', 'MotionDocxExport', 'motions', 'params', 'singleMotion', function ($scope, Config, MotionExportForm, MotionPdfExport, MotionCsvExport, MotionDocxExport, motions, params, singleMotion) { $scope.formFields = MotionExportForm.getFormFields(singleMotion, motions, function () { if ($scope.params.format !== 'pdf') { $scope.params.changeRecommendationMode = 'original'; $scope.params.lineNumberMode = 'none'; $scope.params.include.votingresult = false; } if ($scope.params.format === 'docx') { $scope.params.include.state = false; $scope.params.include.submitter = true; $scope.params.include.motionBlock = false; $scope.params.include.origin = false; $scope.params.include.recommendation = false; } else { $scope.params.include.state = true; $scope.params.include.motionBlock = true; $scope.params.include.origin = true; $scope.params.include.recommendation = true; } if ($scope.params.format === 'pdf') { $scope.params.include.state = true; $scope.params.include.votingresult = true; } }); $scope.params = params || {}; _.defaults($scope.params, { format: 'pdf', pdfFormat: 'pdf', changeRecommendationMode: Config.get('motions_recommendation_text_mode').value, lineNumberMode: Config.get('motions_default_line_numbering').value, amendments: false, include: { text: true, reason: true, state: true, submitters: true, votingresult: true, motionBlock: true, origin: true, recommendation: true, }, includeComments: {}, }); // Always change the mode from agreed to modified_agreed. If a motion does not have a modified // final version, the agreed will be taken. if ($scope.params.changeRecommendationMode === 'agreed') { $scope.params.changeRecommendationMode = 'modified_agreed'; } $scope.motions = motions; $scope.singleMotion = singleMotion; // Add amendments to motions. The amendments are sorted by their identifier var prepareAmendments = function (motions) { var allMotions = []; _.forEach(motions, function (motion) { allMotions.push(motion); allMotions = allMotions.concat( _.sortBy(motion.getAmendments(), function (amendment) { return amendment.identifier; }) ); }); return allMotions; }; $scope.export = function () { if ($scope.params.amendments) { motions = prepareAmendments(motions); } switch ($scope.params.format) { case 'pdf': if ($scope.params.pdfFormat === 'pdf') { MotionPdfExport.export(motions, $scope.params, singleMotion); } else { MotionPdfExport.exportZip(motions, $scope.params); } break; case 'csv': MotionCsvExport.export(motions, $scope.params); break; case 'docx': MotionDocxExport.export(motions, $scope.params); break; } $scope.closeThisDialog(); }; } ]) // Cache for MotionPollDetailCtrl so that users choices are keeped during user actions (e. g. save poll form). .value('MotionPollDetailCtrlCache', {}) // Child controller of MotionDetailCtrl for each single poll. .controller('MotionPollDetailCtrl', [ '$scope', 'MajorityMethodChoices', 'Config', 'MotionPollDetailCtrlCache', 'MotionPollDecimalPlaces', function ($scope, MajorityMethodChoices, Config, MotionPollDetailCtrlCache, MotionPollDecimalPlaces) { // Define choices. $scope.methodChoices = MajorityMethodChoices; // TODO: Get $scope.baseChoices from config_variables.py without copying them. $scope.votesPrecision = MotionPollDecimalPlaces.getPlaces($scope.poll); // Setup empty cache with default values. if (typeof MotionPollDetailCtrlCache[$scope.poll.id] === 'undefined') { MotionPollDetailCtrlCache[$scope.poll.id] = { method: $scope.config('motions_poll_default_majority_method'), }; } // Fetch users choices from cache. $scope.method = MotionPollDetailCtrlCache[$scope.poll.id].method; // Define result function. $scope.isReached = function () { return $scope.poll.isReached($scope.method); }; // Define template controll function $scope.hideMajorityCalculation = function () { return typeof $scope.isReached() === 'undefined' && $scope.method !== 'disabled'; }; // Save current values to cache on detroy of this controller. $scope.$on('$destroy', function() { MotionPollDetailCtrlCache[$scope.poll.id] = { method: $scope.method, }; }); } ]) .controller('MotionListCtrl', [ '$scope', '$state', '$http', 'gettext', 'gettextCatalog', 'operator', 'ngDialog', 'MotionForm', 'Motion', 'MotionComment', 'Category', 'Config', 'Tag', 'Workflow', 'User', 'Agenda', 'MotionBlock', 'Projector', 'ProjectionDefault', 'osTableFilter', 'osTableSort', 'osTablePagination', 'MotionExportForm', 'MotionPdfExport', 'PersonalNoteManager', function($scope, $state, $http, gettext, gettextCatalog, operator, ngDialog, MotionForm, Motion, MotionComment, Category, Config, Tag, Workflow, User, Agenda, MotionBlock, Projector, ProjectionDefault, osTableFilter, osTableSort, osTablePagination, MotionExportForm, MotionPdfExport, PersonalNoteManager) { Category.bindAll({}, $scope, 'categories'); MotionBlock.bindAll({}, $scope, 'motionBlocks'); Tag.bindAll({}, $scope, 'tags'); Workflow.bindAll({}, $scope, 'workflows'); User.bindAll({}, $scope, 'users'); Projector.bindAll({}, $scope, 'projectors'); $scope.$watch(function () { return Projector.lastModified(); }, function () { var projectiondefault = ProjectionDefault.filter({name: 'motions'})[0]; if (projectiondefault) { $scope.defaultProjectorId = projectiondefault.projector_id; } }); $scope.$watch(function () { return Motion.lastModified(); }, function () { // get all main motions and order by identifier (after custom ordering) var motions; if (Config.get('motions_amendments_main_table').value) { motions = Motion.getAll(); } else { motions = Motion.filter({parent_id: undefined}); } $scope.motions = _.orderBy(motions, ['identifier']); _.forEach($scope.motions, function (motion) { MotionComment.populateFields(motion); motion.personalNote = PersonalNoteManager.getNote(motion); // For filtering, we cannot filter for .personalNote.star motion.star = motion.personalNote ? motion.personalNote.star : false; motion.hasPersonalNote = motion.personalNote ? !!motion.personalNote.note : false; if (motion.star === undefined) { motion.star = false; } }); $scope.collectStatesAndRecommendations(); }); $scope.alert = {}; // Motion comments $scope.noSpecialCommentsFields = MotionComment.getNoSpecialCommentsFields(); $scope.showCommentsFilter = function () { return _.keys($scope.noSpecialCommentsFields).length > 0; }; // collect all states and all recommendations of all workflows $scope.collectStatesAndRecommendations = function () { // Special case: If it is the first time updated, update the state filter. // This causes to set the done/undone states correct on page load. var doStateFilterUpdate = !$scope.states; $scope.states = []; $scope.recommendations = []; var workflows = $scope.collectAllUsedWorkflows(); _.forEach(workflows, function (workflow) { if (workflows.length > 1) { var workflowHeader = { headername: workflow.name, workflowHeader: true, }; $scope.states.push(workflowHeader); $scope.recommendations.push(workflowHeader); } var firstEndStateSeen = false; _.forEach(_.orderBy(workflow.states, 'id'), function (state) { if (state.next_states_id.length === 0 && !firstEndStateSeen) { $scope.states.push({divider: true}); firstEndStateSeen = true; } $scope.states.push(state); if (state.recommendation_label) { $scope.recommendations.push(state); } }); }); if (doStateFilterUpdate) { updateStateFilter(); } }; $scope.collectAllUsedWorkflows = function () { return _.filter(Workflow.getAll(), function (workflow) { return _.some($scope.motions, function (motion) { return motion.state.workflow_id === workflow.id; }); }); }; $scope.stateFilter = []; var updateStateFilter = function () { $scope.stateFilter = _.clone($scope.filter.multiselectFilters.state); var doneIndex = _.indexOf($scope.stateFilter, -1); if (doneIndex > -1) { // contains -1 (done) $scope.stateFilter.splice(doneIndex, 1); // remove -1 _.forEach($scope.states, function (state) { if (!state.workflowHeader && !state.divider) { if (state.next_states_id.length === 0) { // add all done state $scope.stateFilter.push(state.id); } } }); } var undoneIndex = _.indexOf($scope.stateFilter, -2); if (undoneIndex > -1) { // contains -2 (undone) $scope.stateFilter.splice(undoneIndex, 1); // remove -2 _.forEach($scope.states, function (state) { if (!state.workflowHeader && !state.divider) { if (state.next_states_id.length !== 0) { // add all undone state $scope.stateFilter.push(state.id); } } }); } $scope.stateFilter = _.uniq($scope.stateFilter); }; // This value may be overritten, so the filters, sorting and pagination in an // derived view are independent to this view. var osTablePrefix = $scope.osTablePrefix || 'MotionTable'; // Filtering $scope.filter = osTableFilter.createInstance(osTablePrefix + 'Filter'); if (!$scope.filter.existsStorageEntry()) { $scope.filter.multiselectFilters = { state: [], category: [], motionBlock: [], tag: [], recommendation: [], comment: [], }; $scope.filter.booleanFilters = { isFavorite: { value: undefined, choiceYes: gettext('Marked as favorite'), choiceNo: gettext('Not marked as favorite'), }, hasPersonalNote: { value: undefined, choiceYes: gettext('Personal note set'), choiceNo: gettext('Personal note not set'), }, }; } $scope.filter.propertyList = ['identifier', 'origin']; $scope.filter.propertyFunctionList = [ function (motion) {return motion.getTitle();}, function (motion) {return motion.category ? motion.category.name : '';}, function (motion) {return motion.motionBlock ? motion.motionBlock.name : '';}, function (motion) {return motion.recommendation ? motion.getRecommendationName() : '';}, ]; $scope.filter.propertyDict = { 'submitters': function (submitter) { return submitter.user.get_short_name(); }, 'supporters': function (supporter) { return supporter.get_short_name(); }, 'tags': function (tag) { return tag.name; }, }; $scope.getItemId = { state: function (motion) {return motion.state_id;}, comment: function (motion) { var ids = []; _.forEach(motion.comments, function (comment, id) { if (comment) { ids.push(id); } }); return ids; }, category: function (motion) {return motion.category_id;}, motionBlock: function (motion) {return motion.motion_block_id;}, tag: function (motion) {return motion.tags_id;}, recommendation: function (motion) {return motion.recommendation_id;}, }; $scope.operateStateFilter = function (id, danger) { $scope.filter.operateMultiselectFilter('state', id, danger); updateStateFilter(); }; $scope.resetFilters = function (danger) { $scope.filter.reset(danger); updateStateFilter(); }; // Sorting $scope.sort = osTableSort.createInstance(osTablePrefix + 'Sort'); if (!$scope.sort.column) { $scope.sort.column = 'identifier'; } $scope.sortOptions = [ {name: 'identifier', display_name: gettext('Identifier')}, {name: 'getTitle()', display_name: gettext('Title')}, {name: 'submitters[0].user.get_short_name()', display_name: gettext('Submitters')}, {name: 'category.' + Config.get('motions_export_category_sorting').value, display_name: gettext('Category')}, {name: 'motionBlock.title', display_name: gettext('Motion block')}, {name: 'state.name', display_name: gettext('State')}, {name: 'log_messages[log_messages.length-1].time', display_name: gettext('Creation date')}, {name: 'log_messages[0].time', display_name: gettext('Last modified')}, ]; // pagination $scope.pagination = osTablePagination.createInstance(osTablePrefix + 'Pagination'); $scope.hasTag = function (motion, tag) { return _.indexOf(motion.tags_id, tag.id) > -1; }; $scope.save = function (motion) { Motion.save(motion, {method: 'PATCH'}); }; // delete single motion $scope.delete = function (motion) { Motion.destroy(motion.id); }; $scope.toggleTag = function (motion, tag) { if ($scope.hasTag(motion, tag)) { // remove motion.tags_id = _.filter(motion.tags_id, function (tag_id){ return tag_id != tag.id; }); } else { motion.tags_id.push(tag.id); } $scope.save(motion); }; $scope.toggleCategory = function (motion, category) { if (motion.category_id == category.id) { motion.category_id = null; } else { motion.category_id = category.id; } $scope.save(motion); }; $scope.toggleMotionBlock = function (motion, block) { if (motion.motion_block_id == block.id) { motion.motion_block_id = null; } else { motion.motion_block_id = block.id; } $scope.save(motion); }; $scope.toggleStar = function (motion) { if (motion.personalNote) { motion.personalNote.star = !motion.personalNote.star; } else { motion.personalNote = {star: true}; } PersonalNoteManager.saveNote(motion, motion.personalNote); }; // open new/edit dialog $scope.openDialog = function (motion) { ngDialog.open(MotionForm.getDialog(motion)); }; // Export dialog $scope.openExportDialog = function (motions) { ngDialog.open(MotionExportForm.getDialog(motions, $scope.sort)); }; $scope.pdfExport = function (motions) { MotionPdfExport.export(motions); }; // *** select mode functions *** $scope.isSelectMode = false; // check all checkboxes from filtered motions $scope.checkAll = function (motions) { $scope.selectedAll = !$scope.selectedAll; _.forEach(motions, function (motion) { motion.selected = $scope.selectedAll; }); }; // uncheck all checkboxes if isSelectMode is closed $scope.uncheckAll = function () { if (!$scope.isSelectMode) { $scope.selectedAll = false; _.forEach($scope.motions, function (motion) { motion.selected = false; }); } }; var selectModeAction = function (motions, predicate) { angular.forEach(motions, function (motion) { if (motion.selected) { predicate(motion); } }); $scope.isSelectMode = false; $scope.uncheckAll(); }; // delete selected motions $scope.deleteMultiple = function (motions) { selectModeAction(motions, function (motion) { $scope.delete(motion); }); }; // set status for selected motions $scope.setStatusMultiple = function (motions, stateId) { selectModeAction(motions, function (motion) { $http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': stateId}); }); }; // set category for selected motions $scope.setCategoryMultiple = function (motions, categoryId) { selectModeAction(motions, function (motion) { motion.category_id = categoryId === 'no_category_selected' ? null : categoryId; $scope.save(motion); }); }; // set status for selected motions $scope.setMotionBlockMultiple = function (motions, motionBlockId) { selectModeAction(motions, function (motion) { motion.motion_block_id = motionBlockId === 'no_motionBlock_selected' ? null : motionBlockId; $scope.save(motion); }); }; } ]) .controller('MotionDetailCtrl', [ '$scope', '$http', '$timeout', '$window', '$filter', 'operator', 'ngDialog', 'gettextCatalog', 'MotionForm', 'AmendmentParagraphChooseForm', 'ChangeRecommendationCreate', 'ChangeRecommendationView', 'MotionStateAndRecommendationParser', 'MotionChangeRecommendation', 'Motion', 'MotionComment', 'Category', 'Mediafile', 'Tag', 'User', 'Workflow', 'Config', 'motionId', 'MotionInlineEditing', 'MotionCommentsInlineEditing', 'Editor', 'Projector', 'ProjectionDefault', 'MotionBlock', 'MotionPdfExport', 'PersonalNoteManager', 'Notify', 'WebpageTitle', 'EditingWarning', function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog, MotionForm, AmendmentParagraphChooseForm, ChangeRecommendationCreate, ChangeRecommendationView, MotionStateAndRecommendationParser, MotionChangeRecommendation, Motion, MotionComment, Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing, MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock, MotionPdfExport, PersonalNoteManager, Notify, WebpageTitle, EditingWarning) { var motion = Motion.get(motionId); Category.bindAll({}, $scope, 'categories'); Mediafile.bindAll({}, $scope, 'mediafiles'); Tag.bindAll({}, $scope, 'tags'); User.bindAll({}, $scope, 'users'); Workflow.bindAll({}, $scope, 'workflows'); MotionBlock.bindAll({}, $scope, 'motionBlocks'); Motion.bindAll({}, $scope, 'motions'); $scope.$watch(function () { return Projector.lastModified(); }, function () { $scope.projectors = Projector.getAll(); var projectiondefault = ProjectionDefault.filter({name: 'motions'})[0]; if (projectiondefault) { $scope.defaultProjectorId = projectiondefault.projector_id; } }); $scope.$watch(function () { return Motion.lastModified(motionId); }, function () { $scope.motion = Motion.get(motionId); $scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff(); MotionComment.populateFields($scope.motion); if (motion.comments) { $scope.stateExtension = $scope.motion.comments[$scope.commentFieldForStateId]; $scope.recommendationExtension = $scope.motion.comments[$scope.commentFieldForRecommendationId]; } $scope.motion.personalNote = PersonalNoteManager.getNote($scope.motion); $scope.navigation.evaluate(); var webpageTitle = gettextCatalog.getString('Motion') + ' '; if ($scope.motion.identifier) { webpageTitle += $scope.motion.identifier + ' - '; } webpageTitle += $scope.motion.getTitle(); WebpageTitle.updateTitle(webpageTitle); $scope.createChangeRecommendation.setVersion(motion, motion.active_version); $scope.viewChangeRecommendations.setVersion(motion, motion.active_version); }); $scope.$watch(function () { return Motion.lastModified(); }, function () { $scope.motions = Motion.getAll(); $scope.amendments = Motion.filter({parent_id: motion.id}); }); $scope.projectionModes = [ {mode: 'original', label: 'Original version'}, {mode: 'changed', label: 'Changed version'}, {mode: 'diff', label: 'Diff version'}, {mode: 'agreed', label: 'Final version'}, {mode: 'modified_agreed', label: 'Modified final version'}, ]; var motionDefaultRecommendationTextMode = Config.get('motions_recommendation_text_mode').value; // Change to the modified final version, if exists if (motionDefaultRecommendationTextMode === 'agreed' && motion.getModifiedFinalVersion()) { motionDefaultRecommendationTextMode = 'modified_agreed'; } $scope.projectionMode = _.find($scope.projectionModes, function (mode) { return mode.mode == motionDefaultRecommendationTextMode; }); if (motion.isProjected().length) { var modeMapping = motion.isProjectedWithMode(); _.forEach($scope.projectionModes, function (mode) { if (mode.mode === modeMapping[0].mode) { $scope.projectionMode = mode; } }); } $scope.setProjectionMode = function (mode) { $scope.projectionMode = mode; var isProjected = motion.isProjectedWithMode(); if (isProjected.length) { _.forEach(isProjected, function (mapping) { if (mapping.mode != mode.mode) { // change the mode if it is different motion.project(mapping.projectorId, mode.mode); } }); } }; $scope.commentsFields = MotionComment.getCommentsFields(); $scope.noSpecialCommentsFields = MotionComment.getNoSpecialCommentsFields(); $scope.commentFieldForStateId = MotionComment.getFieldIdForFlag('forState'); $scope.commentFieldForRecommendationId = MotionComment.getFieldIdForFlag('forRecommendation'); $scope.version = motion.active_version; $scope.isCollapsed = true; $scope.lineNumberMode = Config.get('motions_default_line_numbering').value; $scope.setLineNumberMode = function(mode) { $scope.lineNumberMode = mode; }; $scope.showAmendmentContext = false; $scope.setShowAmendmentContext = function($event) { $event.preventDefault(); $event.stopPropagation(); $scope.showAmendmentContext = !$scope.showAmendmentContext; }; if (motion.parent_id) { Motion.bindOne(motion.parent_id, $scope, 'parent'); } $scope.scrollToLine = 0; $scope.highlight = 0; $scope.linesForProjector = false; $scope.scrollToAndHighlight = function (line) { $scope.scrollToLine = line; $scope.highlight = line; // The same line number can occur twice in diff view; we scroll to the first one in this case var scrollTop = null; $('.line-number-' + line).each(function() { var top = $(this).offset().top; if (top > 0 && (scrollTop === null || top < scrollTop)) { scrollTop = top; } }); if (scrollTop) { // Scroll local; 50 pixel above the line, so it's not completely squeezed to the screen border $('html, body').animate({ 'scrollTop': scrollTop - 50 }, 1000); // remove the line highlight after 2 seconds. $timeout(function () { $scope.highlight = 0; }, 2000); } $scope.scrollProjectorToLine(line); }; $scope.scrollProjectorToLine = function (line) { var projectorIds = $scope.motion.isProjected(); if (!$scope.linesForProjector || !line || !projectorIds.length) { return; } var projectorId = projectorIds[0]; var notifyNamePrefix = 'projector_' + projectorId + '_motion_line_'; // register callback var callbackId = Notify.registerCallback(notifyNamePrefix + 'answer', function (params) { Notify.deregisterCallback(callbackId); $http.post('/rest/core/projector/' + projectorId + '/set_scroll/', params.params.scroll); }); // Query all projectors Notify.notify(notifyNamePrefix + 'request', {line: line}, null, null, [projectorId]); }; $scope.toggleLinesForProjector = function () { $scope.linesForProjector = !$scope.linesForProjector; $scope.scrollProjectorToLine($scope.scrollToLine); }; // open edit dialog $scope.openDialog = function (motion) { if ($scope.inlineEditing.active) { $scope.disableMotionInlineEditing(); } ngDialog.open(MotionForm.getDialog(motion)); }; $scope.save = function (motion) { Motion.save(motion, {method: 'PATCH'}); }; // Navigation buttons $scope.navigation = { evaluate: function () { var motions = $filter('orderByEmptyLast')(Motion.getAll(), 'identifier'); var thisIndex = _.findIndex(motions, function (motion) { return motion.id === $scope.motion.id; }); this.count = motions.length; this.nextMotion = thisIndex < motions.length-1 ? motions[thisIndex+1] : _.head(motions); this.previousMotion = thisIndex > 0 ? motions[thisIndex-1] : _.last(motions); }, }; // support $scope.support = function () { $http.post('/rest/motions/motion/' + motion.id + '/support/'); }; // unsupport $scope.unsupport = function () { $http.delete('/rest/motions/motion/' + motion.id + '/support/'); }; // open dialog for new amendment $scope.newAmendment = function () { var openMainDialog = function (paragraphNo) { var dialog = MotionForm.getDialog(null, motion, paragraphNo); dialog.scope = $scope; ngDialog.open(dialog); }; if (Config.get('motions_amendments_text_mode').value === 'paragraph') { var dialog = AmendmentParagraphChooseForm.getDialog($scope.motion, openMainDialog); dialog.scope = $scope; ngDialog.open(dialog); } else { openMainDialog(); } }; // follow recommendation $scope.followRecommendation = function () { $http.post('/rest/motions/motion/' + motion.id + '/follow_recommendation/', { 'recommendationExtension': $scope.recommendationExtension }); }; // toggle functions for meta information $scope.toggleCategory = function (category) { if ($scope.motion.category_id == category.id) { $scope.motion.category_id = null; } else { $scope.motion.category_id = category.id; } $scope.save($scope.motion); }; $scope.toggleMotionBlock = function (block) { if ($scope.motion.motion_block_id == block.id) { $scope.motion.motion_block_id = null; } else { $scope.motion.motion_block_id = block.id; } $scope.save($scope.motion); }; $scope.toggleTag = function (tag) { if (_.indexOf($scope.motion.tags_id, tag.id) > -1) { // remove $scope.motion.tags_id = _.filter($scope.motion.tags_id, function (tag_id){ return tag_id != tag.id; } ); } else { $scope.motion.tags_id.push(tag.id); } $scope.save($scope.motion); }; // save additional state field $scope.saveAdditionalStateField = function (stateExtension) { motion['comment_' + $scope.commentFieldForStateId] = stateExtension; $scope.save(motion); }; // save additional recommendation field $scope.saveAdditionalRecommendationField = function (recommendationExtension) { motion['comment_' + $scope.commentFieldForRecommendationId] = recommendationExtension; $scope.save(motion); }; $scope.addMotionToRecommendationField = function (motion) { $scope.recommendationExtension += MotionStateAndRecommendationParser.formatMotion(motion); }; // create poll $scope.create_poll = function () { $http.post('/rest/motions/motion/' + motion.id + '/create_poll/', {}); }; // open poll update dialog $scope.openPollDialog = function (poll, voteNumber) { ngDialog.open({ template: 'static/templates/motions/motion-poll-form.html', controller: 'MotionPollUpdateCtrl', className: 'ngdialog-theme-default', closeByEscape: false, closeByDocument: false, resolve: { motionpollId: function () { return poll.id; }, voteNumber: function () { return voteNumber; } } }); }; // delete poll $scope.delete_poll = function (poll) { poll.DSDestroy(); }; // show specific version $scope.showVersion = function (version) { $scope.version = version.id; $scope.inlineEditing.setVersion(motion, version.id); $scope.reasonInlineEditing.setVersion(motion, version.id); $scope.createChangeRecommendation.setVersion(motion, version.id); $scope.viewChangeRecommendations.setVersion(motion, motion.active_version); }; // permit specific version $scope.permitVersion = function (version) { $http.put('/rest/motions/motion/' + motion.id + '/manage_version/', {'version_number': version.version_number}) .then(function(success) { $scope.showVersion(version); }); }; // delete specific version $scope.deleteVersion = function (version) { $http.delete('/rest/motions/motion/' + motion.id + '/manage_version/', {headers: {'Content-Type': 'application/json'}, data: JSON.stringify({version_number: version.version_number})}) .then(function(success) { $scope.showVersion({id: motion.active_version}); }); }; // check if there is at least one comment field $scope.commentFieldsAvailable = function () { return _.keys($scope.noSpecialCommentsFields).length > 0; }; // personal note // For pinning the personal note container we need to adjust the width with JS. We // do not use angular here, because on every window resize a digist cycle would trigger. // This costs too much performance. We use JQuery here, because it is fast for DOM // manipulation and very responsive. $scope.toggleStar = function () { if ($scope.motion.personalNote) { $scope.motion.personalNote.star = !$scope.motion.personalNote.star; } else { $scope.motion.personalNote = {star: true}; } PersonalNoteManager.saveNote($scope.motion, $scope.motion.personalNote); }; $scope.personalNotePinned = false; $scope.pinPersonalNote = function () { $scope.personalNotePinned = !$scope.personalNotePinned; if ($scope.personalNotePinned) { resizePersonalNoteContainer(); } else { $('#personalNote').css('width', ''); } }; $scope.gotoPersonalNote = function () { var pos = $('#personalNote').offset(); $window.scrollTo(pos.left, pos.top); }; var resizePersonalNoteContainer = function () { if ($scope.personalNotePinned) { var width = $('#main-column').width() - 40; // Subtract 2x20px margin $('#personalNote').css('width', width + 'px'); } }; $(window).resize(resizePersonalNoteContainer); // Inline editing functions $scope.inlineEditing = MotionInlineEditing.createInstance($scope, motion, 'view-original-text-inline-editor', true, Editor.getOptions('inline'), function (obj) { return motion.getTextWithLineBreaks($scope.version); }, function (obj) { motion.setTextStrippingLineBreaks(obj.editor.getData()); motion.disable_versioning = (obj.trivialChange && Config.get('motions_allow_disable_versioning').value); } ); $scope.reasonInlineEditing = MotionInlineEditing.createInstance($scope, motion, 'reason-inline-editor', true, Editor.getOptions('inline'), function (obj) { return motion.getReason($scope.version); }, function (obj) { motion.reason = obj.editor.getData(); motion.disable_versioning = (obj.trivialChange && Config.get('motions_allow_disable_versioning').value); } ); $scope.modifiedFinalVersionInlineEditing = MotionInlineEditing.createInstance($scope, motion, 'view-modified-agreed-inline-editor', true, Editor.getOptions('inline'), function (obj) { return motion.getModifiedFinalVersionWithLineBreaks($scope.version); }, function (obj) { motion.setModifiedFinalVersionStrippingLineBreaks(obj.editor.getData()); motion.disable_versioning = (obj.trivialChange && Config.get('motions_allow_disable_versioning').value); } ); // Wrapper functions for $scope.inlineEditing, to warn other users. var editingStoppedCallback; $scope.enableMotionInlineEditing = function () { editingStoppedCallback = EditingWarning.editingStarted('motion_update_' + motion.id); if ($scope.motion.getReason($scope.version)) { $scope.reasonInlineEditing.enable(); } $scope.inlineEditing.enable(); }; $scope.disableMotionInlineEditing = function () { if (editingStoppedCallback) { editingStoppedCallback(); } if ($scope.motion && $scope.motion.getReason($scope.version)) { $scope.reasonInlineEditing.disable(); } $scope.inlineEditing.disable(); }; $scope.textReasonSaveToolbarVisible = function () { return ($scope.inlineEditing.changed && $scope.inlineEditing.active) || ($scope.reasonInlineEditing.changed && $scope.reasonInlineEditing.active); }; $scope.textReasonSave = function () { if ($scope.motion.getReason($scope.version)) { $scope.reasonInlineEditing.save(); } $scope.inlineEditing.save(); }; $scope.textReasonRevert = function () { if ($scope.motion.getReason($scope.version)) { $scope.reasonInlineEditing.revert(); } $scope.inlineEditing.revert(); }; $scope.commentsInlineEditing = MotionCommentsInlineEditing.createInstances($scope, motion); $scope.personalNoteInlineEditing = MotionInlineEditing.createInstance($scope, motion, 'personal-note-inline-editor', false, Editor.getOptions('inline'), function (obj) { return motion.personalNote ? motion.personalNote.note : ''; }, function (obj) { if (motion.personalNote) { motion.personalNote.note = obj.editor.getData(); } else { motion.personalNote = {note: obj.editor.getData()}; } PersonalNoteManager.saveNote(motion, motion.personalNote); obj.revert(); obj.disable(); return true; // Do not update the motion via patch request. } ); // Change recommendation creation functions $scope.createChangeRecommendation = ChangeRecommendationCreate; $scope.createChangeRecommendation.init($scope, motion); // Change recommendation and amendment viewing $scope.viewChangeRecommendations = ChangeRecommendationView; $scope.viewChangeRecommendations.initSite($scope, motion, motionDefaultRecommendationTextMode); // PDF creating functions $scope.pdfExport = function () { var identifier = $scope.motion.identifier ? '-' + $scope.motion.identifier : ''; var params = { filename: gettextCatalog.getString('Motion') + identifier + '.pdf', version: $scope.version, changeRecommendationMode: $scope.viewChangeRecommendations.mode, lineNumberMode: $scope.lineNumberMode, }; MotionPdfExport.export(motion, params, true); }; $scope.createPollPdf = function () { MotionPdfExport.createPollPdf($scope.motion, $scope.version); }; $scope.exportComment = function (commentId) { var identifier = $scope.motion.identifier ? '-' + $scope.motion.identifier : ''; var commentsString = ' - ' + gettextCatalog.getString('Comments'); var filename = gettextCatalog.getString('Motion') + identifier + commentsString + '.pdf'; MotionPdfExport.exportComment($scope.motion, commentId, filename); }; $scope.exportPersonalNote = function () { var identifier = $scope.motion.identifier ? '-' + $scope.motion.identifier : ''; var personalNoteString = ' - ' + gettextCatalog.getString('personal note'); var filename = gettextCatalog.getString('Motion') + identifier + personalNoteString + '.pdf'; MotionPdfExport.exportPersonalNote($scope.motion, filename); }; } ]) .controller('ChangeRecommendationTitleUpdateCtrl', [ '$scope', 'MotionChangeRecommendation', 'ChangeRecommendationTitleForm', 'diffService', 'change', 'ErrorMessage', function ($scope, MotionChangeRecommendation, ChangeRecommendationTitleForm, diffService, change, ErrorMessage) { $scope.alert = {}; $scope.model = angular.copy(change); // get all form fields $scope.formFields = ChangeRecommendationTitleForm.getFormFields(); // save motion $scope.save = function (change) { // inject the changed change recommendation (copy) object back into DS store MotionChangeRecommendation.inject(change); // save changed change recommendation object on server MotionChangeRecommendation.save(change).then( function() { $scope.closeThisDialog(); }, function (error) { MotionChangeRecommendation.refresh(change); $scope.alert = ErrorMessage.forAlert(error); } ); }; } ]) .controller('ChangeRecommendationTitleCreateCtrl', [ '$scope', 'Motion', 'MotionChangeRecommendation', 'ChangeRecommendationTitleForm', 'Config', 'diffService', 'motion', 'version', function($scope, Motion, MotionChangeRecommendation, ChangeRecommendationTitleForm, Config, diffService, motion, version) { $scope.alert = {}; $scope.model = { text: version.title, motion_version_id: version.id }; // get all form fields $scope.formFields = ChangeRecommendationTitleForm.getFormFields(); // save motion $scope.save = function (change) { change.line_from = 0; change.line_to = 0; MotionChangeRecommendation.create(change).then( function() { $scope.closeThisDialog(); } ); }; } ]) .controller('ChangeRecommendationTextUpdateCtrl', [ '$scope', 'MotionChangeRecommendation', 'ChangeRecommendationTextForm', 'diffService', 'change', 'ErrorMessage', function ($scope, MotionChangeRecommendation, ChangeRecommendationTextForm, diffService, change, ErrorMessage) { $scope.alert = {}; $scope.model = angular.copy(change); $scope.model._change_object = undefined; // get all form fields $scope.formFields = ChangeRecommendationTextForm.getFormFields(change.line_from, change.line_to); // save motion $scope.save = function (change) { change.text = diffService.removeDuplicateClassesInsertedByCkeditor(change.text); // inject the changed change recommendation (copy) object back into DS store MotionChangeRecommendation.inject(change); // save changed change recommendation object on server MotionChangeRecommendation.save(change).then( function() { $scope.closeThisDialog(); }, function (error) { MotionChangeRecommendation.refresh(change); $scope.alert = ErrorMessage.forAlert(error); } ); }; } ]) .controller('ChangeRecommendationTextCreateCtrl', [ '$scope', 'Motion', 'MotionChangeRecommendation', 'ChangeRecommendationTextForm', 'Config', 'diffService', 'motion', 'version', 'lineFrom', 'lineTo', function($scope, Motion, MotionChangeRecommendation, ChangeRecommendationTextForm, Config, diffService, motion, version, lineFrom, lineTo) { $scope.alert = {}; var html = motion.getTextWithLineBreaks(version.id), lineData = diffService.extractRangeByLineNumbers(html, lineFrom, lineTo); $scope.model = { text: lineData.outerContextStart + lineData.innerContextStart + lineData.html + lineData.innerContextEnd + lineData.outerContextEnd, line_from: lineFrom, line_to: lineTo, motion_version_id: version.id, type: 0 }; // get all form fields $scope.formFields = ChangeRecommendationTextForm.getFormFields(lineFrom, lineTo); // save motion $scope.save = function (motion) { motion.text = diffService.removeDuplicateClassesInsertedByCkeditor(motion.text); MotionChangeRecommendation.create(motion).then( function() { $scope.closeThisDialog(); } ); }; } ]) .controller('AmendmentParagraphChooseCtrl', [ '$scope', '$state', 'Motion', 'motion', 'successCb', function($scope, $state, Motion, motion, successCb) { $scope.model = angular.copy(motion); $scope.model.paragraph_selected = null; $scope.paragraphs = motion.getTextParagraphs(motion.active_version, true).map(function(text, index) { // This prevents an error in ng-repeater's duplication detection if two identical paragraphs occur return { "paragraphNo": index, "text": text }; }); $scope.gotoMotionForm = function() { var paragraphNo = parseInt($scope.model.paragraph_selected); successCb(paragraphNo); $scope.closeThisDialog(); }; } ]) .controller('MotionCreateCtrl', [ '$scope', '$state', 'gettext', 'gettextCatalog', 'operator', 'Motion', 'MotionForm', 'parentMotion', 'paragraphNo', 'paragraphTextPre', 'Category', 'Config', 'Mediafile', 'Tag', 'User', 'Workflow', 'Agenda', 'ErrorMessage', function($scope, $state, gettext, gettextCatalog, operator, Motion, MotionForm, parentMotion, paragraphNo, paragraphTextPre, Category, Config, Mediafile, Tag, User, Workflow, Agenda, ErrorMessage) { Category.bindAll({}, $scope, 'categories'); Mediafile.bindAll({}, $scope, 'mediafiles'); Tag.bindAll({}, $scope, 'tags'); User.bindAll({}, $scope, 'users'); Workflow.bindAll({}, $scope, 'workflows'); $scope.model = { agenda_type: parseInt(Config.get('agenda_new_items_default_visibility').value), }; $scope.alert = {}; // Check whether this is a new amendment. var isAmendment = parentMotion && parentMotion.id, isParagraphBasedAmendment = false; // Set default values for create form // ... for amendments add parent_id if (isAmendment) { if (Config.get('motions_amendments_text_mode').value === 'fulltext') { $scope.model.text = parentMotion.getText(); } if (Config.get('motions_amendments_text_mode').value === 'paragraph' && paragraphNo !== undefined) { var paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false); $scope.model.text = paragraphs[paragraphNo]; isParagraphBasedAmendment = true; } if (paragraphTextPre !== undefined) { $scope.model.text = paragraphTextPre; } if (parentMotion.identifier) { $scope.model.title = gettextCatalog.getString('Amendment to') + ' ' + parentMotion.identifier; } else { $scope.model.title = gettextCatalog.getString('Amendment to motion ') + ' ' + parentMotion.getTitle(); } $scope.model.paragraphNo = paragraphNo; $scope.model.parent_id = parentMotion.id; $scope.model.category_id = parentMotion.category_id; $scope.model.motion_block_id = parentMotion.motion_block_id; Motion.bindOne($scope.model.parent_id, $scope, 'parent'); } // ... preselect default workflow if exist var workflow = Workflow.get(Config.get('motions_workflow').value); if (!workflow) { workflow = _.first(Workflow.getAll()); } if (workflow) { $scope.model.workflow_id = workflow.id; } else { $scope.alert = { type: 'danger', msg: gettextCatalog.getString('No workflows exists. You will not ' + 'be able to create a motion.'), show: true, }; } // get all form fields $scope.formFields = MotionForm.getFormFields(true, isParagraphBasedAmendment); // save motion $scope.save = function (motion, gotoDetailView) { if (isAmendment && motion.paragraphNo !== undefined) { var orig_paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false); motion.amendment_paragraphs = orig_paragraphs.map(function (_, idx) { return (idx === motion.paragraphNo ? motion.text : null); }); } // The attribute motion.agenda_parent_id is set by the form, see form definition. Motion.create(motion).then( function(success) { if (isAmendment || gotoDetailView) { $state.go('motions.motion.detail', {id: success.id}); } $scope.closeThisDialog(); }, function (error) { $scope.alert = ErrorMessage.forAlert(error); } ); }; } ]) .controller('MotionUpdateCtrl', [ '$scope', '$state', 'Motion', 'Category', 'Config', 'Mediafile', 'MotionForm', 'Tag', 'User', 'Workflow', 'Agenda', 'motionId', 'operator', 'ErrorMessage', 'EditingWarning', function ($scope, $state, Motion, Category, Config, Mediafile, MotionForm, Tag, User, Workflow, Agenda, motionId, operator, ErrorMessage, EditingWarning) { Category.bindAll({}, $scope, 'categories'); Mediafile.bindAll({}, $scope, 'mediafiles'); Tag.bindAll({}, $scope, 'tags'); User.bindAll({}, $scope, 'users'); Workflow.bindAll({}, $scope, 'workflows'); $scope.alert = {}; // set initial values for form model by create deep copy of motion object // so list/detail view is not updated while editing var motion = Motion.get(motionId); // We need to clone this by hand. angular and lodash are not capable of keeping // crossreferences out. $scope.model = { id: motion.id, parent_id: motion.parent_id, identifier: motion.identifier, title: motion.getTitle(), text: motion.getText(), reason: motion.getReason(), submitters_id: _.map(motion.submitters_id), supporters_id: _.map(motion.supporters_id), tags_id: _.map(motion.tags_id), state_id: motion.state_id, recommendation_id: motion.recommendation_id, origin: motion.origin, workflow_id: motion.workflow_id, comments: _.clone(motion.comments), attachments_id: _.map(motion.attachments_id), active_version: motion.active_version, agenda_item_id: motion.agenda_item_id, category_id: motion.category_id, motion_block_id: motion.motion_block_id, }; // Clone comments _.forEach(motion.comments, function (comment, index) { $scope.model['comment_' + index] = comment; }); $scope.model.disable_versioning = false; $scope.model.more = false; if (motion.isParagraphBasedAmendment()) { motion.getVersion(motion.active_version).amendment_paragraphs.forEach(function(paragraph_amend, paragraphNo) { // Hint: this assumes there is only one modified paragraph if (paragraph_amend !== null) { $scope.model.text = paragraph_amend; $scope.model.paragraphNo = paragraphNo; } }); $scope.model.title = motion.getTitle(); } // get all form fields $scope.formFields = MotionForm.getFormFields(false, motion.isParagraphBasedAmendment()); // override default values for update form for (var i = 0; i < $scope.formFields.length; i++) { if ($scope.formFields[i].key == "identifier") { // show identifier field if the operator has manage permissions $scope.formFields[i].hide = !operator.hasPerms('motions.can_manage'); } if ($scope.formFields[i].key == "title") { // get title of latest version $scope.formFields[i].defaultValue = motion.getTitle(-1); } if ($scope.formFields[i].key == "text") { // get text of latest version $scope.formFields[i].defaultValue = motion.getText(-1); } if ($scope.formFields[i].key == "reason") { // get reason of latest version $scope.formFields[i].defaultValue = motion.getReason(-1); } if ($scope.formFields[i].key == "disable_versioning") { if (Config.get('motions_allow_disable_versioning').value && motion.state.versioning) { // check current state if versioning is active $scope.formFields[i].hide = false; } } if ($scope.formFields[i].key == "workflow_id") { // get saved workflow id from state $scope.formFields[i].defaultValue = motion.state.workflow_id; } } // Displaying a warning, if other users edit this motion too var editingStoppedCallback = EditingWarning.editingStarted('motion_update_' + motionId); $scope.$on('$destroy', editingStoppedCallback); // Save motion $scope.save = function (model, gotoDetailView) { if ($scope.model.paragraphNo !== undefined) { var parentMotion = motion.getParentMotion(); var orig_paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false); $scope.model.amendment_paragraphs = orig_paragraphs.map(function (_, idx) { return (idx === $scope.model.paragraphNo ? $scope.model.text : null); }); } // inject the changed motion (copy) object back into DS store Motion.inject(model); // save changed motion object on server Motion.save(model).then( function(success) { if (gotoDetailView) { $state.go('motions.motion.detail', {id: success.id}); } $scope.closeThisDialog(); }, function (error) { // save error: revert all changes by restore // (refresh) original motion object from server Motion.refresh(model); $scope.alert = ErrorMessage.forAlert(error); } ); }; } ]) .controller('MotionCommentCtrl', [ '$scope', 'Motion', 'MotionComment', 'MotionCommentForm', 'motionId', 'commentFieldId', 'gettextCatalog', 'ErrorMessage', function ($scope, Motion, MotionComment, MotionCommentForm, motionId, commentFieldId, gettextCatalog, ErrorMessage) { $scope.alert = {}; // set initial values for form model by create deep copy of motion object // so list/detail view is not updated while editing var motion = Motion.get(motionId); $scope.model = angular.copy(motion); $scope.formFields = MotionCommentForm.getFormFields(commentFieldId); var fields = MotionComment.getNoSpecialCommentsFields(); var title = gettextCatalog.getString('Edit comment %%comment%% of motion %%motion%%'); title = title.replace('%%comment%%', fields[commentFieldId].name); $scope.title = title.replace('%%motion%%', motion.getTitle()); $scope.model.title = motion.getTitle(-1); $scope.model.text = motion.getText(-1); $scope.model.reason = motion.getReason(-1); if (motion.isParagraphBasedAmendment()) { motion.getVersion(motion.active_version).amendment_paragraphs.forEach(function(paragraph_amend, paragraphNo) { // Hint: this assumes there is only one modified paragraph if (paragraph_amend !== null) { $scope.model.text = paragraph_amend; $scope.model.paragraphNo = paragraphNo; } }); } $scope.save = function (motion) { if (motion.isParagraphBasedAmendment()) { motion.getVersion(motion.active_version).amendment_paragraphs.forEach(function(paragraph_amend, paragraphNo) { // Hint: this assumes there is only one modified paragraph if (paragraph_amend !== null) { $scope.model.text = paragraph_amend; $scope.model.paragraphNo = paragraphNo; } }); } // inject the changed motion (copy) object back into DS store Motion.inject(motion); // save changed motion object on server Motion.save(motion).then( function(success) { $scope.closeThisDialog(); }, function (error) { // save error: revert all changes by restore // (refresh) original motion object from server Motion.refresh(motion); $scope.alert = ErrorMessage.forAlert(error); } ); }; } ]) .controller('MotionPollUpdateCtrl', [ '$scope', 'gettextCatalog', 'MotionPoll', 'MotionPollForm', 'MotionPollDecimalPlaces', 'motionpollId', 'voteNumber', 'ErrorMessage', function ($scope, gettextCatalog, MotionPoll, MotionPollForm, MotionPollDecimalPlaces, motionpollId, voteNumber, ErrorMessage) { // set initial values for form model by create deep copy of motionpoll object // so detail view is not updated while editing poll var motionpoll = MotionPoll.get(motionpollId); $scope.model = angular.copy(motionpoll); $scope.voteNumber = voteNumber; var precision = MotionPollDecimalPlaces.getPlaces(motionpoll); $scope.formFields = MotionPollForm.getFormFields(precision); $scope.alert = {}; // save motionpoll $scope.save = function (poll) { poll.DSUpdate({ motion_id: poll.motion_id, votes: {"Yes": poll.yes, "No": poll.no, "Abstain": poll.abstain}, 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); }); }; } ]) .controller('MotionSubmitterCtrl', [ '$scope', '$filter', '$http', 'User', 'Motion', 'motionId', 'ErrorMessage', function ($scope, $filter, $http, User, Motion, motionId, ErrorMessage) { User.bindAll({}, $scope, 'users'); $scope.submitterSelectBox = {}; $scope.alert = {}; $scope.$watch(function () { return Motion.lastModified(motionId); }, function () { $scope.motion = Motion.get(motionId); $scope.submitters = $filter('orderBy')($scope.motion.submitters, 'weight'); }); $scope.addSubmitter = function (userId) { $scope.submitterSelectBox = {}; $http.post('/rest/motions/motion/' + $scope.motion.id + '/manage_submitters/', { 'user': userId }).then( function (success) { $scope.alert.show = false; }, function (error) { $scope.alert = ErrorMessage.forAlert(error); } ); }; $scope.removeSubmitter = function (userId) { $http.delete('/rest/motions/motion/' + $scope.motion.id + '/manage_submitters/', { headers: {'Content-Type': 'application/json'}, data: JSON.stringify({user: userId}) }).then( function (success) { $scope.alert.show = false; }, function (error) { $scope.alert = ErrorMessage.forAlert(error); } ); }; // save reordered list of submitters $scope.treeOptions = { dropped: function (event) { var submitterIds = _.map($scope.submitters, function (submitter) { return submitter.id; }); $http.post('/rest/motions/motion/' + $scope.motion.id + '/sort_submitters/', { submitters: submitterIds, }); } }; } ]) .controller('MotionAmendmentListStateCtrl', [ '$scope', 'motionId', function ($scope, motionId) { $scope.motionId = motionId; $scope.osTablePrefix = 'AmendmentTable'; } ]) .controller('MotionAmendmentListCtrl', [ '$scope', '$sessionStorage', '$state', 'Motion', 'MotionComment', 'MotionForm', 'PersonalNoteManager', 'ngDialog', 'MotionCommentForm', 'MotionChangeRecommendation', 'MotionPdfExport', 'AmendmentCsvExport', 'gettextCatalog', 'gettext', function ($scope, $sessionStorage, $state, Motion, MotionComment, MotionForm, PersonalNoteManager, ngDialog, MotionCommentForm, MotionChangeRecommendation, MotionPdfExport, AmendmentCsvExport, gettextCatalog, gettext) { if ($scope.motionId) { $scope.leadMotion = Motion.get($scope.motionId); } var updateMotions = function () { // check, if lead motion is given var amendments; if ($scope.leadMotion) { amendments = Motion.filter({parent_id: $scope.leadMotion.id}); } else { amendments = _.filter(Motion.getAll(), function (motion) { return motion.parent_id; }); } // always order by identifier (after custom ordering) $scope.amendments = _.orderBy(amendments, ['identifier']); _.forEach($scope.amendments, function (amendment) { MotionComment.populateFields(amendment); amendment.personalNote = PersonalNoteManager.getNote(amendment); // For filtering, we cannot filter for .personalNote.star amendment.star = amendment.personalNote ? amendment.personalNote.star : false; amendment.hasPersonalNote = amendment.personalNote ? !!amendment.personalNote.note : false; if (amendment.star === undefined) { amendment.star = false; } // add a custom sort attribute var parentMotion = amendment.getParentMotion(); amendment.parentMotionAndLineNumber = parentMotion.identifier; if (amendment.isParagraphBasedAmendment()) { var paragraphs = amendment.getAmendmentParagraphsLinesDiff(); var diffLine = '0'; if (paragraphs.length) { diffLine = '' + paragraphs[0].diffLineFrom; } while (diffLine.length < 6) { diffLine = '0' + diffLine; } amendment.parentMotionAndLineNumber += ' ' + diffLine; } }); // Get all lead motions $scope.leadMotions = _.orderBy(Motion.filter({parent_id: undefined}), ['identifier']); //updateCollissions(); }; var updateCollissions = function () { $scope.collissions = {}; _.forEach($scope.amendments, function (amendment) { if (amendment.isParagraphBasedAmendment()) { var parentMotion = amendment.getParentMotion(); // get all change recommendations _and_ changes by amendments from the // parent motion. From all get the unified change object. var parentChangeRecommendations = _.filter( MotionChangeRecommendation.filter({ 'where': {'motion_version_id': {'==': parentMotion.active_version}} }), function (change) { return change.isTextRecommendation(); } ); var parentChanges = parentChangeRecommendations.map(function (cr) { return cr.getUnifiedChangeObject(); }).concat( _.map(parentMotion.getParagraphBasedAmendmentsForDiffView(), function (amendment) { return amendment.getUnifiedChangeObject(); }) ); var change = amendment.getUnifiedChangeObject(); if (change) { change.setOtherChangesForCollission(parentChanges); $scope.collissions[amendment.id] = !!change.getCollissions().length; } } }); }; //$scope.$watch(function () { // return MotionChangeRecommendation.lastModified(); //}, updateCollissions); $scope.$watch(function () { return Motion.lastModified(); }, updateMotions); $scope.selectLeadMotion = function (motion) { $scope.leadMotion = motion; updateMotions(); if ($scope.leadMotion) { $state.transitionTo('motions.motion.amendment-list', {id: $scope.leadMotion.id}, {notify: false} ); } else { $state.transitionTo('motions.motion.allamendments', {}, {notify: false} ); } }; // Save expand state so the session if ($sessionStorage.amendmentTableExpandState) { $scope.toggleExpandContent(); } $scope.saveExpandState = function (state) { $sessionStorage.amendmentTableExpandState = state; }; // add custom sorting $scope.sortOptions.unshift({ name: 'parentMotionAndLineNumber', display_name: gettext('Parent motion and line number'), }); if (!$scope.sort.column || $scope.sort.column === 'identifier') { $scope.sort.column = 'parentMotionAndLineNumber'; } $scope.isTextExpandable = function (comment, characters) { comment = $(comment).text(); return comment.length > characters; }; $scope.getTextPreview = function (comment, characters) { comment = $(comment).text(); if (comment.length > characters) { comment = comment.substr(0, characters) + '...'; } return comment; }; $scope.editComment = function (motion, fieldId) { ngDialog.open(MotionCommentForm.getDialog(motion, fieldId)); }; $scope.createModifiedAmendment = function (amendment) { var paragraphNo, paragraphText; if (amendment.isParagraphBasedAmendment()) { // We assume there is only one affected paragraph amendment.getVersion(amendment.active_version).amendment_paragraphs.forEach(function(parText, parNo) { if (parText !== null) { paragraphNo = parNo; paragraphText = parText; } }); } else { paragraphText = amendment.getText(); } ngDialog.open(MotionForm.getDialog(null, amendment.getParentMotion(), paragraphNo, paragraphText)); }; $scope.amendmentPdfExport = function (motions) { var filename; if ($scope.leadMotion) { filename = gettextCatalog.getString('Amendments to') + ' ' + $scope.leadMotion.getTitle(); } else { filename = gettextCatalog.getString('Amendments'); } filename += '.pdf'; MotionPdfExport.exportAmendments(motions, filename); }; $scope.exportCsv = function (motions) { AmendmentCsvExport.export(motions); }; } ]) .controller('MotionImportCtrl', [ '$scope', '$q', 'gettext', 'Category', 'Motion', 'MotionBlock', 'User', 'MotionCsvExport', function ($scope, $q, gettext, Category, Motion, MotionBlock, User, MotionCsvExport) { // set initial data for csv import $scope.motions = []; // set csv $scope.csvConfig = { accept: '.csv, .txt', encodingOptions: ['UTF-8', 'ISO-8859-1'], parseConfig: { skipEmptyLines: true, }, }; var FIELDS = ['identifier', 'title', 'text', 'reason', 'submitter', 'category', 'origin', 'motionBlock']; $scope.motions = []; $scope.onCsvChange = function (csv) { $scope.motions = []; var motions = []; _.forEach(csv.data, function (row) { if (row.length >= 3) { var filledRow = _.zipObject(FIELDS, row); motions.push(filledRow); } }); _.forEach(motions, function (motion) { motion.selected = true; // identifier if (motion.identifier !== '') { // All motion objects are already loaded via the resolve statement from ui-router. var motions = Motion.getAll(); if (_.find(motions, function (item) { return item.identifier === motion.identifier; })) { motion.importerror = true; motion.identifier_error = gettext('Error: Identifier already exists.'); } } // title if (!motion.title) { motion.importerror = true; motion.title_error = gettext('Error: Title is required.'); } // text if (!motion.text) { motion.importerror = true; motion.text_error = gettext('Error: Text is required.'); } else if (!motion.text.startsWith('

')) { motion.text = '

' + motion.text + '

'; } // Reason if (motion.reason && !motion.reason.startsWith('

')) { motion.reason = '

' + motion.reason + '

'; } // submitter if (motion.submitter && motion.submitter !== '') { _.forEach(User.getAll(), function (user) { var user_short_name = [user.title, user.first_name, user.last_name].join(' ').trim(); if (user_short_name == motion.submitter.trim()) { motion.submitters_id = [user.id]; motion.submitter = user.full_name; } }); if (!motion.submitters_id) { motion.submitter_create = gettext('New participant will be created.'); } } // category if (motion.category && motion.category !== '') { angular.forEach(Category.getAll(), function (category) { // search for existing category if (category.name == motion.category.trim()) { motion.category_id = category.id; motion.category = category.name; } }); if (!motion.category_id) { motion.category_create = gettext('New category will be created.'); } } // Motion block if (motion.motionBlock && motion.motionBlock !== '') { angular.forEach(MotionBlock.getAll(), function (block) { // search for existing block if (block.title == motion.motionBlock.trim()) { motion.motion_block_id = block.id; motion.motionBlock = block.title; } }); if (!motion.motion_block_id) { motion.motionBlock_create = gettext('New motion block will be created.'); } } $scope.motions.push(motion); }); $scope.calcStats(); }; $scope.calcStats = function () { $scope.motionsWillNotBeImported = 0; $scope.motionsWillBeImported = 0; $scope.motions.forEach(function(motion) { if (!motion.importerror && motion.selected) { $scope.motionsWillBeImported++; } else { $scope.motionsWillNotBeImported++; } }); }; // Counter for creations $scope.usersCreated = 0; $scope.categoriesCreated = 0; // import from csv file $scope.import = function () { $scope.csvImporting = true; // Reset counters $scope.usersCreated = 0; $scope.categoriesCreated = 0; $scope.motionBlocksCreated = 0; var importedUsers = []; var importedCategories = []; var importedMotionBlocks = []; // collect users, categories and motion blocks angular.forEach($scope.motions, function (motion) { if (motion.selected && !motion.importerror) { // collect user if not exists if (!motion.submitters_id && motion.submitter) { var index = motion.submitter.indexOf(' '); var user = { first_name: motion.submitter.substr(0, index), last_name: motion.submitter.substr(index+1), groups_id: [] }; importedUsers.push(user); } // collect category if not exists if (!motion.category_id && motion.category) { var category = { name: motion.category, prefix: motion.category.charAt(0) }; importedCategories.push(category); } // collect motion block if not exists if (!motion.motion_block_id && motion.motionBlock) { var motionBlock = { title: motion.motionBlock, }; importedMotionBlocks.push(motionBlock); } } }); // unique users, categories and motion blocks var importedUsersUnique = _.uniqWith(importedUsers, function (u1, u2) { return u1.first_name == u2.first_name && u1.last_name == u2.last_name; }); var importedCategoriesUnique = _.uniqWith(importedCategories, function (c1, c2) { return c1.name == c2.name; }); var importedMotionBlocksUnique = _.uniqWith(importedMotionBlocks, function (c1, c2) { return c1.title == c2.title; }); // Promises for users and categories var createPromises = []; // create users and categories _.forEach(importedUsersUnique, function (user) { createPromises.push(User.create(user).then( function (success) { user.id = success.id; $scope.usersCreated++; } )); }); _.forEach(importedCategoriesUnique, function (category) { createPromises.push(Category.create(category).then( function (success) { category.id = success.id; $scope.categoriesCreated++; } )); }); _.forEach(importedMotionBlocksUnique, function (motionBlock) { createPromises.push(MotionBlock.create(motionBlock).then( function (success) { motionBlock.id = success.id; $scope.motionBlocksCreated++; } )); }); // wait for users and categories to create $q.all(createPromises).then( function() { angular.forEach($scope.motions, function (motion) { if (motion.selected && !motion.importerror) { // now, add user if (!motion.submitters_id && motion.submitter) { var index = motion.submitter.indexOf(' '); var first_name = motion.submitter.substr(0, index); var last_name = motion.submitter.substr(index+1); // search for user, set id. _.forEach(importedUsersUnique, function (user) { if (user.first_name == first_name && user.last_name == last_name) { motion.submitters_id = [user.id]; } }); } // add category if (!motion.category_id && motion.category) { var name = motion.category; // search for category, set id. _.forEach(importedCategoriesUnique, function (category) { if (category.name == name) { motion.category_id = category.id; } }); } // add motion block if (!motion.motion_block_id && motion.motionBlock) { var title = motion.motionBlock; // search for motion block _.forEach(importedMotionBlocksUnique, function (motionBlock) { if (motionBlock.title == title) { motion.motion_block_id = motionBlock.id; } }); } // finally create motion Motion.create(motion).then( function(success) { motion.imported = true; } ); } }); }); $scope.csvimported = true; }; $scope.clear = function () { $scope.motions = []; }; // download CSV example file $scope.downloadCSVExample = function () { MotionCsvExport.downloadExample(); }; } ]) .controller('CategoryListCtrl', [ '$scope', 'Category', 'ngDialog', 'CategoryForm', function($scope, Category, ngDialog, CategoryForm) { Category.bindAll({}, $scope, 'categories'); // setup table sorting $scope.sortColumn = 'name'; $scope.reverse = false; // function to sort by clicked column $scope.toggleSort = function (column) { if ($scope.sortColumn === column) { $scope.reverse = !$scope.reverse; } $scope.sortColumn = column; }; // delete selected category $scope.delete = function (category) { Category.destroy(category.id); }; $scope.editOrCreate = function (category) { ngDialog.open(CategoryForm.getDialog(category)); }; } ]) .controller('CategoryCreateCtrl', [ '$scope', 'Category', 'CategoryForm', 'ErrorMessage', function($scope, Category, CategoryForm, ErrorMessage) { $scope.model = {}; $scope.alert = {}; $scope.formFields = CategoryForm.getFormFields(); $scope.save = function (category) { Category.create(category).then( function (success) { $scope.closeThisDialog(); }, function (error) { $scope.alert = ErrorMessage.forAlert(error); } ); }; } ]) .controller('CategoryUpdateCtrl', [ '$scope', 'Category', 'categoryId', 'CategoryForm', 'ErrorMessage', function ($scope, Category, categoryId, CategoryForm, ErrorMessage) { $scope.alert = {}; $scope.model = angular.copy(Category.get(categoryId)); $scope.formFields = CategoryForm.getFormFields(); $scope.save = function (category) { Category.inject(category); Category.save(category).then( function (success) { $scope.closeThisDialog(); }, function (error) { // save error: revert all changes by restore // (refresh) original category object from server Category.refresh(category); $scope.alert = ErrorMessage.forAlert(error); } ); }; } ]) .controller('CategorySortCtrl', [ '$scope', '$stateParams', '$http', 'Category', 'categoryId', 'Motion', 'ErrorMessage', function ($scope, $stateParams, $http, Category, categoryId, Motion, ErrorMessage) { Category.bindOne(categoryId, $scope, 'category'); Motion.bindAll({}, $scope, 'motions'); $scope.filter = { category_id: categoryId, parent_id: null, orderBy: 'identifier' }; $scope.$watch(function () { return Motion.lastModified(); }, function () { var motions = Motion.filter($scope.filter); $scope.items = _.map(motions, function (motion) { return { id: motion.id, item: motion }; }); }); $scope.alert = {}; // Numbers all motions in this category by the given order in $scope.items $scope.numbering = function () { // Create a list of all motion ids in the current order. var sorted_motions = []; $scope.items.forEach(function (item) { sorted_motions.push(item.item.id); }); // renumber them $http.post('/rest/motions/category/' + $scope.category.id + '/numbering/', {'motions': sorted_motions} ).then( function (success) { $scope.alert = { type: 'success', msg: success.data.detail, show: true }; }, function (error) { $scope.alert = ErrorMessage.forAlert(error); }); }; } ]) //mark all motions config strings for translation in javascript .config([ 'gettext', function (gettext) { gettext('Motions'); // subgroup General gettext('General'); gettext('Workflow of new motions'); gettext('Identifier'); gettext('Numbered per category'); gettext('Serially numbered'); gettext('Set it manually'); gettext('Motion preamble'); gettext('The assembly may decide:'); gettext('Default line numbering'); /// Line numbering: Outside gettext('Outside'); /// Line numbering: Inline gettext('Inline'); /// Line numbering: None gettext('None'); gettext('Line length'); gettext('The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40'); gettext('Hide reason on projector'); gettext('Hide meta information box on projector'); gettext('Hide recommendation on projector'); gettext('Stop submitting new motions by non-staff users'); gettext('Allow to disable versioning'); gettext('Name of recommender'); gettext('Default text version for change recommendations'); gettext('Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.'); gettext('Edit comment %%comment%% of motion %%motion%%'); // subgroup Amendments gettext('Amendments'); gettext('Activate amendments'); gettext('Show amendments together with motions'); gettext('Prefix for the identifier for amendments'); gettext('Apply text for new amendments'); gettext('The title of the motion is always applied.'); gettext('Amendment to'); gettext('How to create new amendments'); gettext('Empty text field'); gettext('Edit the whole motion text'); gettext('Paragraph-based, Diff-enabled'); // subgroup Supporters gettext('Supporters'); gettext('Number of (minimum) required supporters for a motion'); gettext('Choose 0 to disable the supporting system.'); gettext('Remove all supporters of a motion if a submitter edits his ' + 'motion in early state'); // subgroup Supporters gettext('Comments'); gettext('Comment fields for motions'); gettext('Public'); gettext('Private'); // subgroup Voting and ballot papers gettext('Voting and ballot papers'); gettext('The 100 % base of a voting result consists of'); gettext('Yes/No/Abstain'); gettext('Yes/No'); gettext('All valid ballots'); gettext('All casted ballots'); gettext('Disabled (no percents)'); gettext('Required majority'); gettext('Default method to check whether a motion has reached the required majority.'); gettext('Simple majority'); gettext('Two-thirds majority'); gettext('Three-quarters majority'); gettext('Disabled'); 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'); // subgroup PDF and DOCX gettext('Title for PDF and DOCX documents (all motions)'); gettext('Preamble text for PDF and DOCX documents (all motions)'); gettext('Sort categories by'); gettext('Include the sequential number in PDF and DOCX'); // misc strings (used dynamically in templates by translate filter) gettext('needed'); gettext('Amendment'); } ]); }());