3315 lines
132 KiB
JavaScript
3315 lines
132 KiB
JavaScript
(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: "<ui-view/>",
|
|
data: {
|
|
title: gettext('Motions'),
|
|
basePerm: 'motions.can_see',
|
|
},
|
|
})
|
|
.state('motions.motion', {
|
|
abstract: true,
|
|
template: "<ui-view/>",
|
|
})
|
|
.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: "<ui-view/>",
|
|
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: '<ui-view/>',
|
|
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: '<ui-view/>',
|
|
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: '<p class="spacer-top-lg no-padding">' + Config.translate(Config.get('motions_preamble').value) + '</p>'
|
|
},
|
|
{
|
|
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: '<hr class="smallhr">',
|
|
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('<p>')) {
|
|
motion.text = '<p>' + motion.text + '</p>';
|
|
}
|
|
// Reason
|
|
if (motion.reason && !motion.reason.startsWith('<p>')) {
|
|
motion.reason = '<p>' + motion.reason + '</p>';
|
|
}
|
|
// 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');
|
|
}
|
|
]);
|
|
|
|
}());
|