Agenda DOCX export, motion log message when comment edited, motion sequential number in detail view and PDF, save pagination state to session storage (fixes #3558).

This commit is contained in:
FinnStutzenstein 2018-01-26 14:56:42 +01:00 committed by Emanuel Schütze
parent df523ce526
commit 3ba39c37c1
25 changed files with 320 additions and 114 deletions

View File

@ -14,6 +14,7 @@ Agenda:
- New permission for managing lists of speakers [#3366]. - New permission for managing lists of speakers [#3366].
- Fixed multiple request on creation of agenda related items [#3341]. - Fixed multiple request on creation of agenda related items [#3341].
- Added possibility to mark speakers [#3570]. - Added possibility to mark speakers [#3570].
- New DOCX export of agenda [#3569].
Motions: Motions:
- New export dialog [#3185]. - New export dialog [#3185].
@ -56,6 +57,8 @@ Motions:
- Combined all boolean filters into one dropdown menu and added a filter - Combined all boolean filters into one dropdown menu and added a filter
for amendments [#3501]. for amendments [#3501].
- Allow to delete own motions [#3516]. - Allow to delete own motions [#3516].
- Log which comment was updated [#3569].
- Save pagination sate to session storage [#3569].
Elections: Elections:
- Added pagination for list view [#3393]. - Added pagination for list view [#3393].

View File

@ -0,0 +1,84 @@
(function () {
'use strict';
angular.module('OpenSlidesApp.agenda.docx', ['OpenSlidesApp.core.docx'])
.factory('AgendaDocxExport', [
'$http',
'gettextCatalog',
'FileSaver',
'Agenda',
'AgendaTree',
'Config',
function ($http, gettextCatalog, FileSaver, Agenda, AgendaTree, Config) {
var getData = function (items) {
// Item structure: The top layer has subitems, that are flat.
// The first layer is bold and all sublayers not. The docx
// templater cannot render items recursively, so the second
// layer are all subitems flated out. Spacing is done with tabs.
var tree = AgendaTree.getTree(items);
var subitems = []; // This will be used as a temporary variable.
var flatSubitems = function (children, parentCount) {
_.forEach(children, function (child) {
var taps = _.repeat('\t', parentCount - 1);
subitems.push({
item_number: taps + child.item.item_number,
item_title: child.item.list_view_title,
});
flatSubitems(child.children, parentCount + 1);
});
};
var twoLayerTree = _.map(tree, function (mainItem) {
subitems = [];
flatSubitems(mainItem.children, 1);
return {
item_number: mainItem.item.item_number,
item_title: mainItem.item.list_view_title,
subitems: subitems,
};
});
// header
var headerline1 = [
Config.translate(Config.get('general_event_name').value),
Config.translate(Config.get('general_event_description').value)
].filter(Boolean).join(' ');
var headerline2 = [
Config.get('general_event_location').value,
Config.get('general_event_date').value
].filter(Boolean).join(', ');
// Data structure for the docx templater.
return {
header: [headerline1, headerline2].join('\n'),
agenda_translation: gettextCatalog.getString('Agenda'),
top_list: twoLayerTree,
};
};
return {
export: function (items) {
// TODO: use filtered items.
var filename = gettextCatalog.getString('Agenda') + '.docx';
$http.get('/agenda/docxtemplate/').then(function (success) {
var content = window.atob(success.data);
var doc = new Docxgen(content);
var data = getData(items);
doc.setData(data);
doc.render();
var zip = doc.getZip();
//zip = converter.updateZipFile(zip);
var out = zip.generate({type: 'blob'});
FileSaver.saveAs(out, filename);
});
},
};
}
]);
})();

View File

@ -7,6 +7,7 @@ angular.module('OpenSlidesApp.agenda.site', [
'OpenSlidesApp.core.pdf', 'OpenSlidesApp.core.pdf',
'OpenSlidesApp.agenda.pdf', 'OpenSlidesApp.agenda.pdf',
'OpenSlidesApp.agenda.csv', 'OpenSlidesApp.agenda.csv',
'OpenSlidesApp.agenda.docx',
]) ])
.config([ .config([
@ -100,12 +101,14 @@ angular.module('OpenSlidesApp.agenda.site', [
'gettextCatalog', 'gettextCatalog',
'gettext', 'gettext',
'osTableFilter', 'osTableFilter',
'osTablePagination',
'AgendaCsvExport', 'AgendaCsvExport',
'AgendaPdfExport', 'AgendaPdfExport',
'AgendaDocxExport',
'ErrorMessage', 'ErrorMessage',
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm,
AgendaTree, Projector, ProjectionDefault, gettextCatalog, gettext, osTableFilter, AgendaTree, Projector, ProjectionDefault, gettextCatalog, gettext, osTableFilter,
AgendaCsvExport, AgendaPdfExport, ErrorMessage) { osTablePagination, AgendaCsvExport, AgendaPdfExport, AgendaDocxExport, ErrorMessage) {
// Bind agenda tree to the scope // Bind agenda tree to the scope
$scope.$watch(function () { $scope.$watch(function () {
return Agenda.lastModified(); return Agenda.lastModified();
@ -167,13 +170,7 @@ angular.module('OpenSlidesApp.agenda.site', [
}; };
// pagination // pagination
$scope.currentPage = 1; $scope.pagination = osTablePagination.createInstance('AgendaTablePagination');
$scope.itemsPerPage = 25;
$scope.limitBegin = 0;
$scope.pageChanged = function() {
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
$scope.gotoTop();
};
// parse duration for inline editing // parse duration for inline editing
$scope.generateDurationText = function (item) { $scope.generateDurationText = function (item) {
@ -288,6 +285,9 @@ angular.module('OpenSlidesApp.agenda.site', [
$scope.csvExport = function () { $scope.csvExport = function () {
AgendaCsvExport.export($scope.itemsFiltered); AgendaCsvExport.export($scope.itemsFiltered);
}; };
$scope.docxExport = function () {
AgendaDocxExport.export($scope.itemsFiltered);
};
/** select mode functions **/ /** select mode functions **/
$scope.isSelectMode = false; $scope.isSelectMode = false;

View File

@ -111,6 +111,12 @@
CSV CSV
</a> </a>
</li> </li>
<li>
<a href="" ng-click="docxExport()">
<i class="fa fa-file-word-o"></i>
DOCX
</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -143,9 +149,10 @@
</span> </span>
</span> </span>
</div> </div>
<div class="col-md-6" ng-show="itemsFiltered.length > itemsPerPage"> <div class="col-md-6" ng-show="itemsFiltered.length > pagination.itemsPerPage">
<span class="pull-right"> <span class="pull-right">
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(itemsFiltered.length/itemsPerPage) }} <translate>Page</translate> {{ pagination.currentPage }} /
{{ Math.ceil(itemsFiltered.length/pagination.itemsPerPage) }}
</span> </span>
</div> </div>
</div> </div>
@ -225,7 +232,7 @@
| osFilter: filter.filterString : filter.getObjectQueryString | osFilter: filter.filterString : filter.getObjectQueryString
| filter: {closed: filter.booleanFilters.closed.value} | filter: {closed: filter.booleanFilters.closed.value}
| filter: {is_hidden: filter.booleanFilters.is_hidden.value}) | filter: {is_hidden: filter.booleanFilters.is_hidden.value})
| limitTo : itemsPerPage : limitBegin"> | limitTo : pagination.itemsPerPage : pagination.limitBegin">
<!-- select column --> <!-- select column -->
<div ng-show="isSelectMode" os-perms="agenda.can_manage" class="col-xs-1 centered"> <div ng-show="isSelectMode" os-perms="agenda.can_manage" class="col-xs-1 centered">
@ -368,11 +375,11 @@
</div> <!-- container --> </div> <!-- container -->
<ul uib-pagination <ul uib-pagination
ng-show="itemsFiltered.length > itemsPerPage" ng-show="itemsFiltered.length > pagination.itemsPerPage"
total-items="itemsFiltered.length" total-items="itemsFiltered.length"
items-per-page="itemsPerPage" items-per-page="pagination.itemsPerPage"
ng-model="currentPage" ng-model="pagination.currentPage"
ng-change="pageChanged()" ng-change="pagination.pageChanged()"
class="pagination-sm" class="pagination-sm"
direction-links="false" direction-links="false"
boundary-links="true" boundary-links="true"

Binary file not shown.

View File

@ -0,0 +1,9 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^docxtemplate/$',
views.AgendaDocxTemplateView.as_view(),
name='agenda_docx_template'),
]

View File

@ -15,6 +15,7 @@ from openslides.utils.rest_api import (
detail_route, detail_route,
list_route, list_route,
) )
from openslides.utils.views import BinaryTemplateView
from ..utils.auth import has_perm from ..utils.auth import has_perm
from .access_permissions import ItemAccessPermissions from .access_permissions import ItemAccessPermissions
@ -311,3 +312,11 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
inform_changed_data(items) inform_changed_data(items)
return Response({'detail': _('The agenda has been sorted.')}) return Response({'detail': _('The agenda has been sorted.')})
# Special views
class AgendaDocxTemplateView(BinaryTemplateView):
"""
Returns the template for motions docx export
"""
template_name = 'templates/docx/agenda.docx'

View File

@ -274,11 +274,13 @@ angular.module('OpenSlidesApp.assignments.site', [
'User', 'User',
'osTableFilter', 'osTableFilter',
'osTableSort', 'osTableSort',
'osTablePagination',
'gettext', 'gettext',
'AssignmentPhases', 'AssignmentPhases',
'AssignmentPdfExport', 'AssignmentPdfExport',
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, Projector, ProjectionDefault, function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, Projector,
gettextCatalog, User, osTableFilter, osTableSort, gettext, AssignmentPhases, AssignmentPdfExport) { ProjectionDefault, gettextCatalog, User, osTableFilter, osTableSort, osTablePagination,
gettext, AssignmentPhases, AssignmentPdfExport) {
$scope.$watch(function () { $scope.$watch(function () {
return Assignment.lastModified(); return Assignment.lastModified();
}, function () { }, function () {
@ -354,13 +356,7 @@ angular.module('OpenSlidesApp.assignments.site', [
}; };
// Pagination // Pagination
$scope.currentPage = 1; $scope.pagination = osTablePagination.createInstance('AssignmentTablePagination');
$scope.itemsPerPage = 25;
$scope.limitBegin = 0;
$scope.pageChanged = function() {
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
$scope.gotoTop();
};
// update phase // update phase
$scope.updatePhase = function (assignment, phase_id) { $scope.updatePhase = function (assignment, phase_id) {

View File

@ -77,9 +77,10 @@
{{ assignments.length }} {{ "elections" | translate }}<span ng-if="(assignments|filter:{selected:true}).length > 0">, {{ assignments.length }} {{ "elections" | translate }}<span ng-if="(assignments|filter:{selected:true}).length > 0">,
{{(assignments|filter:{selected:true}).length}} {{ "selected" | translate }}</span> {{(assignments|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
</div> </div>
<div class="col-md-6" ng-show="assignmentsFiltered.length > itemsPerPage"> <div class="col-md-6" ng-show="assignmentsFiltered.length > pagination.itemsPerPage">
<span class="pull-right"> <span class="pull-right">
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(assignmentsFiltered.length/itemsPerPage) }} <translate>Page</translate> {{ pagination.currentPage }} /
{{ Math.ceil(assignmentsFiltered.length/pagination.itemsPerPage) }}
</span> </span>
</div> </div>
</div> </div>
@ -87,7 +88,7 @@
<div class="os-table container-fluid"> <div class="os-table container-fluid">
<div class="row header-row"> <div class="row header-row">
<div class="col-xs-1 centered" ng-show="isSelectMode"> <div class="col-xs-1 centered" ng-show="isSelectMode">
<i class="fa text-danger pointer" ng-class=" selectedAll ? 'fa-check-square-o' : 'fa-square-o'" <i class="fa text-danger pointer" ng-class="selectedAll ? 'fa-check-square-o' : 'fa-square-o'"
ng-click="checkAll()"></i> ng-click="checkAll()"></i>
</div> </div>
<div class="col-xs-11 main-header"> <div class="col-xs-11 main-header">
@ -216,7 +217,7 @@
| MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag | MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag
| MultiselectFilter: filter.multiselectFilters.phase : getItemId.phase | MultiselectFilter: filter.multiselectFilters.phase : getItemId.phase
| orderByEmptyLast: sort.column : sort.reverse) | orderByEmptyLast: sort.column : sort.reverse)
| limitTo : itemsPerPage : limitBegin"> | limitTo : pagination.itemsPerPage : pagination.limitBegin">
<!-- select column --> <!-- select column -->
<div ng-show="isSelectMode" os-perms="assignments.can_manage" class="col-xs-1 centered"> <div ng-show="isSelectMode" os-perms="assignments.can_manage" class="col-xs-1 centered">
@ -332,11 +333,11 @@
</div> <!-- main table --> </div> <!-- main table -->
<ul uib-pagination <ul uib-pagination
ng-show="assignmentsFiltered.length > itemsPerPage" ng-show="assignmentsFiltered.length > pagination.itemsPerPage"
total-items="assignmentsFiltered.length" total-items="assignmentsFiltered.length"
items-per-page="itemsPerPage" items-per-page="pagination.itemsPerPage"
ng-model="currentPage" ng-model="pagination.currentPage"
ng-change="pageChanged()" ng-change="pagination.pageChanged()"
class="pagination-sm" class="pagination-sm"
direction-links="false" direction-links="false"
boundary-links="true" boundary-links="true"

View File

@ -76,6 +76,8 @@ class CoreAppConfig(AppConfig):
# Client settings # Client settings
client_settings_keys = [ client_settings_keys = [
'MOTION_IDENTIFIER_MIN_DIGITS',
'MOTION_IDENTIFIER_WITHOUT_BLANKS',
'MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS' 'MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS'
] ]
client_settings_dict = {} client_settings_dict = {}

View File

@ -612,6 +612,47 @@ angular.module('OpenSlidesApp.core.site', [
} }
]) ])
/* Factory for pagination of the tables. Saves all settings (currentPage, ...)
* to the session storage and recovers them when the table is reloaded.
* You have to provide a 'tableName' where the settings are saved in the session
* storage. Has to be unique for obvious reasons.
* The 'itemsPerPage' is optional. If not given, it defaults to 25.
*/
.factory('osTablePagination', [
'$rootScope',
'$sessionStorage',
function ($rootScope, $sessionStorage) {
var createInstance = function (tableName, itemsPerPage) {
// Defaults
var self = {
currentPage: 1,
itemsPerPage: itemsPerPage || 25,
limitBegin: 0,
};
// Check storage; maybe recover old state.
var storage = $sessionStorage[tableName];
if (storage) {
self = storage;
}
self.save = function () {
$sessionStorage[tableName] = self;
};
self.pageChanged = function () {
self.limitBegin = (self.currentPage - 1) * self.itemsPerPage;
self.save();
$rootScope.gotoTop();
};
return self;
};
return {
createInstance: createInstance
};
}
])
/* This Factory could be used in any dialog, if the user should be warned, if another user /* This Factory could be used in any dialog, if the user should be warned, if another user
* also has this dialog open. Use it like in this example in any dialog controller: * also has this dialog open. Use it like in this example in any dialog controller:
var editingStoppedCallback = EditingWarning.editingStarted('editing_name' + item.id); var editingStoppedCallback = EditingWarning.editingStarted('editing_name' + item.id);

View File

@ -17,13 +17,14 @@ angular.module('OpenSlidesApp.mediafiles.list', [
'ngDialog', 'ngDialog',
'osTableFilter', 'osTableFilter',
'osTableSort', 'osTableSort',
'osTablePagination',
'ProjectionDefault', 'ProjectionDefault',
'Projector', 'Projector',
'User', 'User',
'Mediafile', 'Mediafile',
'MediafileForm', 'MediafileForm',
'Logos', 'Logos',
function ($http, $scope, gettext, ngDialog, osTableFilter, osTableSort, function ($http, $scope, gettext, ngDialog, osTableFilter, osTableSort, osTablePagination,
ProjectionDefault, Projector, User, Mediafile, MediafileForm, Logos) { ProjectionDefault, Projector, User, Mediafile, MediafileForm, Logos) {
$scope.$watch(function () { $scope.$watch(function () {
return Mediafile.lastModified(); return Mediafile.lastModified();
@ -107,13 +108,7 @@ angular.module('OpenSlidesApp.mediafiles.list', [
]; ];
// pagination // pagination
$scope.currentPage = 1; $scope.pagination = osTablePagination.createInstance('MediafileTablePagination');
$scope.itemsPerPage = 25;
$scope.limitBegin = 0;
$scope.pageChanged = function() {
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
$scope.gotoTop();
};
// open new/edit dialog // open new/edit dialog
$scope.openDialog = function (mediafile) { $scope.openDialog = function (mediafile) {

View File

@ -144,9 +144,10 @@
{{ mediafiles.length }} {{ "files" | translate }}<span ng-if="(mediafiles|filter:{selected:true}).length > 0">, {{ mediafiles.length }} {{ "files" | translate }}<span ng-if="(mediafiles|filter:{selected:true}).length > 0">,
{{(mediafiles|filter:{selected:true}).length}} {{ "selected" | translate }}</span> {{(mediafiles|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
</div> </div>
<div class="col-md-6" ng-show="mediafilesFiltered.length > itemsPerPage"> <div class="col-md-6" ng-show="mediafilesFiltered.length > pagination.itemsPerPage">
<span class="pull-right"> <span class="pull-right">
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(mediafilesFiltered.length/itemsPerPage) }} <translate>Page</translate> {{ pagination.currentPage }} /
{{ Math.ceil(mediafilesFiltered.length/pagination.itemsPerPage) }}
</span> </span>
</div> </div>
</div> </div>
@ -248,7 +249,7 @@
| filter: {filetype: (filter.booleanFilters.isPdf.value ? 'application/pdf' : (filter.booleanFilters.isPdf.value === false ? '!application/pdf' : ''))} | filter: {filetype: (filter.booleanFilters.isPdf.value ? 'application/pdf' : (filter.booleanFilters.isPdf.value === false ? '!application/pdf' : ''))}
| filter: {hidden: filter.booleanFilters.isHidden.value} | filter: {hidden: filter.booleanFilters.isHidden.value}
| orderByEmptyLast: sort.column : sort.reverse) | orderByEmptyLast: sort.column : sort.reverse)
| limitTo: itemsPerPage : limitBegin"> | limitTo: pagination.itemsPerPage : pagination.limitBegin">
<!-- select column --> <!-- select column -->
<div ng-show="isSelectMode" os-perms="mediafiles.can_manage" class="col-xs-1 centered"> <div ng-show="isSelectMode" os-perms="mediafiles.can_manage" class="col-xs-1 centered">
@ -368,11 +369,11 @@
</div><!-- end os-table --> </div><!-- end os-table -->
<ul uib-pagination <ul uib-pagination
ng-show="mediafilesFiltered.length > itemsPerPage" ng-show="mediafilesFiltered.length > pagination.itemsPerPage"
total-items="mediafilesFiltered.length" total-items="mediafilesFiltered.length"
items-per-page="itemsPerPage" items-per-page="pagination.itemsPerPage"
ng-model="currentPage" ng-model="pagination.currentPage"
ng-change="pageChanged()" ng-change="pagination.pageChanged()"
class="pagination-sm" class="pagination-sm"
direction-links="false" direction-links="false"
boundary-links="true" boundary-links="true"

View File

@ -272,6 +272,14 @@ angular.module('OpenSlidesApp.motions', [
title += this.getTitle(); title += this.getTitle();
return title; return title;
}, },
getSequentialNumber: function () {
var id = this.id + '';
var zeros = Math.max(0, OpenSlidesSettings.MOTION_IDENTIFIER_MIN_DIGITS - id.length);
for (var i = 0; i < zeros; i++) {
id = '0' + id;
}
return id;
},
getText: function (versionId) { getText: function (versionId) {
return this.getVersion(versionId).text; return this.getVersion(versionId).text;
}, },

View File

@ -16,8 +16,9 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'Config', 'Config',
'Motion', 'Motion',
'MotionComment', 'MotionComment',
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, HTMLValidizer, 'OpenSlidesSettings',
Category, Config, Motion, MotionComment) { function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter,
HTMLValidizer, Category, Config, Motion, MotionComment, OpenSlidesSettings) {
/** /**
* Provides the content as JS objects for Motions in pdfMake context * Provides the content as JS objects for Motions in pdfMake context
* @constructor * @constructor
@ -77,7 +78,8 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
); );
} }
if (Config.get('motions_export_sequential_number').value) { if (Config.get('motions_export_sequential_number').value) {
subtitleLines.push(gettextCatalog.getString('Sequential number') + ': ' + motion.id); subtitleLines.push(gettextCatalog.getString('Sequential number') + ': ' +
motion.getSequentialNumber());
} }
var subtitle = PDFLayout.createSubtitle(subtitleLines); var subtitle = PDFLayout.createSubtitle(subtitleLines);

View File

@ -962,13 +962,14 @@ angular.module('OpenSlidesApp.motions.site', [
'ProjectionDefault', 'ProjectionDefault',
'osTableFilter', 'osTableFilter',
'osTableSort', 'osTableSort',
'osTablePagination',
'MotionExportForm', 'MotionExportForm',
'MotionPdfExport', 'MotionPdfExport',
'PersonalNoteManager', 'PersonalNoteManager',
function($scope, $state, $http, gettext, gettextCatalog, operator, ngDialog, MotionForm, Motion, function($scope, $state, $http, gettext, gettextCatalog, operator, ngDialog, MotionForm, Motion,
MotionComment, Category, Config, Tag, Workflow, User, Agenda, MotionBlock, Projector, MotionComment, Category, Config, Tag, Workflow, User, Agenda, MotionBlock, Projector,
ProjectionDefault, osTableFilter, osTableSort, MotionExportForm, MotionPdfExport, ProjectionDefault, osTableFilter, osTableSort, osTablePagination, MotionExportForm,
PersonalNoteManager) { MotionPdfExport, PersonalNoteManager) {
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
MotionBlock.bindAll({}, $scope, 'motionBlocks'); MotionBlock.bindAll({}, $scope, 'motionBlocks');
Tag.bindAll({}, $scope, 'tags'); Tag.bindAll({}, $scope, 'tags');
@ -1177,14 +1178,7 @@ angular.module('OpenSlidesApp.motions.site', [
]; ];
// pagination // pagination
$scope.currentPage = 1; $scope.pagination = osTablePagination.createInstance('MotionTablePagination');
$scope.itemsPerPage = 25;
$scope.limitBegin = 0;
$scope.pageChanged = function() {
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
$scope.gotoTop();
};
// update state // update state
$scope.updateState = function (motion, state_id) { $scope.updateState = function (motion, state_id) {
@ -1497,6 +1491,7 @@ angular.module('OpenSlidesApp.motions.site', [
var thisIndex = _.findIndex(motions, function (motion) { var thisIndex = _.findIndex(motions, function (motion) {
return motion.id === $scope.motion.id; return motion.id === $scope.motion.id;
}); });
this.count = motions.length;
this.nextMotion = thisIndex < motions.length-1 ? motions[thisIndex+1] : _.head(motions); this.nextMotion = thisIndex < motions.length-1 ? motions[thisIndex+1] : _.head(motions);
this.previousMotion = thisIndex > 0 ? motions[thisIndex-1] : _.last(motions); this.previousMotion = thisIndex > 0 ? motions[thisIndex-1] : _.last(motions);
}, },

View File

@ -78,10 +78,13 @@
<i class="fa fa-exclamation-triangle"></i> <i class="fa fa-exclamation-triangle"></i>
<translate>This version is not permitted.</translate> <translate>This version is not permitted.</translate>
</span> </span>
<small>
<translate>Sequential number</translate> {{ motion.getSequentialNumber() }}
</small>
</h2> </h2>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<span class="pull-right"> <span class="pull-right" ng-if="navigation.count > 2">
<a ui-sref="motions.motion.detail({id: navigation.previousMotion.id})" <a ui-sref="motions.motion.detail({id: navigation.previousMotion.id})"
class="btn btn-default btn-sm" ng-disabled="!navigation.previousMotion"> class="btn btn-default btn-sm" ng-disabled="!navigation.previousMotion">
<i class="fa fa-angle-double-left"></i> <i class="fa fa-angle-double-left"></i>

View File

@ -126,9 +126,10 @@
{{ motions.length }} {{ "motions" | translate }}<span ng-if="(motions|filter:{selected:true}).length > 0">, {{ motions.length }} {{ "motions" | translate }}<span ng-if="(motions|filter:{selected:true}).length > 0">,
{{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}</span> {{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
</div> </div>
<div class="col-md-6" ng-show="motionsFiltered.length > itemsPerPage"> <div class="col-md-6" ng-show="motionsFiltered.length > pagination.itemsPerPage">
<span class="pull-right"> <span class="pull-right">
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(motionsFiltered.length/itemsPerPage) }} <translate>Page</translate> {{ pagination.currentPage }} /
{{ Math.ceil(motionsFiltered.length/pagination.itemsPerPage) }}
</span> </span>
</div> </div>
</div> </div>
@ -532,7 +533,7 @@
| filter: {isAmendment: filter.booleanFilters.isAmendment.value} | filter: {isAmendment: filter.booleanFilters.isAmendment.value}
| toArray | toArray
| orderByEmptyLast: sort.column : sort.reverse) | orderByEmptyLast: sort.column : sort.reverse)
| limitTo : itemsPerPage : limitBegin"> | limitTo : pagination.itemsPerPage : pagination.limitBegin">
<!-- select column --> <!-- select column -->
<div ng-show="isSelectMode" os-perms="motions.can_manage" class="col-xs-1 centered"> <div ng-show="isSelectMode" os-perms="motions.can_manage" class="col-xs-1 centered">
@ -788,11 +789,11 @@
</div> <!-- data row --> </div> <!-- data row -->
<ul uib-pagination <ul uib-pagination
ng-show="motionsFiltered.length > itemsPerPage" ng-show="motionsFiltered.length > pagination.itemsPerPage"
total-items="motionsFiltered.length" total-items="motionsFiltered.length"
items-per-page="itemsPerPage" items-per-page="pagination.itemsPerPage"
ng-model="currentPage" ng-model="pagination.currentPage"
ng-change="pageChanged()" ng-change="pagination.pageChanged()"
class="pagination-sm" class="pagination-sm"
direction-links="false" direction-links="false"
boundary-links="true" boundary-links="true"

View File

@ -1,9 +1,7 @@
import base64
import re import re
from typing import Optional # noqa from typing import Optional # noqa
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.http import Http404 from django.http import Http404
@ -24,7 +22,7 @@ from ..utils.rest_api import (
ValidationError, ValidationError,
detail_route, detail_route,
) )
from ..utils.views import APIView from ..utils.views import BinaryTemplateView
from .access_permissions import ( from .access_permissions import (
CategoryAccessPermissions, CategoryAccessPermissions,
MotionAccessPermissions, MotionAccessPermissions,
@ -206,6 +204,16 @@ class MotionViewSet(ModelViewSet):
# No comments here. Just do nothing. # No comments here. Just do nothing.
pass pass
# get changed comment fields
changed_comment_fields = []
comments = request.data.get('comments', {})
for id, value in comments.items():
if not motion.comments or motion.comments.get(id) != value:
field = config['motions_comments'].get(id)
if field:
name = field['name']
changed_comment_fields.append(name)
# Validate data and update motion. # Validate data and update motion.
serializer = self.get_serializer( serializer = self.get_serializer(
motion, motion,
@ -222,6 +230,11 @@ class MotionViewSet(ModelViewSet):
updated_motion.supporters.clear() updated_motion.supporters.clear()
updated_motion.write_log([ugettext_noop('All supporters removed')], request.user) updated_motion.write_log([ugettext_noop('All supporters removed')], request.user)
if len(changed_comment_fields) > 0:
updated_motion.write_log(
[ugettext_noop('Comment {} updated').format(', '.join(changed_comment_fields))],
request.user)
# Send new submitters and supporters via autoupdate because users # Send new submitters and supporters via autoupdate because users
# without permission to see users may not have them but can get it now. # without permission to see users may not have them but can get it now.
new_users = list(updated_motion.submitters.all()) new_users = list(updated_motion.submitters.all())
@ -707,15 +720,10 @@ class WorkflowViewSet(ModelViewSet):
return result return result
# Special API views # Special views
class MotionDocxTemplateView(APIView): class MotionDocxTemplateView(BinaryTemplateView):
""" """
Returns the template for motions docx export Returns the template for motions docx export
""" """
http_method_names = ['get'] template_name = 'templates/docx/motions.docx'
def get_context_data(self, **context):
with open(finders.find('templates/docx/motions.docx'), "rb") as file:
response = base64.b64encode(file.read())
return response

View File

@ -12,6 +12,7 @@ urlpatterns += [
url(r'^%s(?P<path>.*)$' % settings.MEDIA_URL.lstrip('/'), protected_serve, {'document_root': settings.MEDIA_ROOT}), url(r'^%s(?P<path>.*)$' % settings.MEDIA_URL.lstrip('/'), protected_serve, {'document_root': settings.MEDIA_ROOT}),
url(r'^(?P<url>.*[^/])$', RedirectView.as_view(url='/%(url)s/', permanent=True)), url(r'^(?P<url>.*[^/])$', RedirectView.as_view(url='/%(url)s/', permanent=True)),
url(r'^rest/', include(router.urls)), url(r'^rest/', include(router.urls)),
url(r'^agenda/', include('openslides.agenda.urls')),
url(r'^motions/', include('openslides.motions.urls')), url(r'^motions/', include('openslides.motions.urls')),
url(r'^users/', include('openslides.users.urls')), url(r'^users/', include('openslides.users.urls')),

View File

@ -551,12 +551,13 @@ angular.module('OpenSlidesApp.users.site', [
'UserCsvExport', 'UserCsvExport',
'osTableFilter', 'osTableFilter',
'osTableSort', 'osTableSort',
'osTablePagination',
'gettext', 'gettext',
'UserPdfExport', 'UserPdfExport',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, $http, $q, ngDialog, UserForm, User, Group, PasswordGenerator, function($scope, $state, $http, $q, ngDialog, UserForm, User, Group, PasswordGenerator,
Projector, ProjectionDefault, Config, gettextCatalog, UserCsvExport, osTableFilter, Projector, ProjectionDefault, Config, gettextCatalog, UserCsvExport, osTableFilter,
osTableSort, gettext, UserPdfExport, ErrorMessage) { osTableSort, osTablePagination, gettext, UserPdfExport, ErrorMessage) {
$scope.$watch(function () { $scope.$watch(function () {
return User.lastModified(); return User.lastModified();
}, function () { }, function () {
@ -649,13 +650,7 @@ angular.module('OpenSlidesApp.users.site', [
]; ];
// pagination // pagination
$scope.currentPage = 1; $scope.pagination = osTablePagination.createInstance('UserTablePagination');
$scope.itemsPerPage = 25;
$scope.limitBegin = 0;
$scope.pageChanged = function() {
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
$scope.gotoTop();
};
// Toggle group from user // Toggle group from user
$scope.toggleGroup = function (user, group) { $scope.toggleGroup = function (user, group) {
@ -1121,15 +1116,13 @@ angular.module('OpenSlidesApp.users.site', [
'User', 'User',
'Group', 'Group',
'UserCsvExport', 'UserCsvExport',
'osTablePagination',
'ErrorMessage', 'ErrorMessage',
function($scope, $http, $q, gettext, gettextCatalog, User, Group, UserCsvExport, ErrorMessage) { function($scope, $http, $q, gettext, gettextCatalog, User, Group, UserCsvExport,
osTablePagination, ErrorMessage) {
// import from textarea // import from textarea
$scope.importByLine = function () { $scope.importByLine = function () {
var usernames = $scope.userlist[0].split("\n"); var usernames = $scope.userlist[0].split("\n");
// Ignore empty lines.
/*usernames = _.filter(usernames, function (name) {
return name !== '';
});*/
var users = _.map(usernames, function (name) { var users = _.map(usernames, function (name) {
// Split each full name in first and last name. // Split each full name in first and last name.
// The last word is set as last name, rest is the first name(s). // The last word is set as last name, rest is the first name(s).
@ -1157,12 +1150,9 @@ angular.module('OpenSlidesApp.users.site', [
}; };
// pagination // pagination
$scope.currentPage = 1; $scope.pagination = osTablePagination.createInstance('UserImportTablePagination', 100);
$scope.itemsPerPage = 100;
$scope.limitBegin = 0; // Duplicates
$scope.pageChanged = function() {
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
};
$scope.duplicateActions = [ $scope.duplicateActions = [
gettext('keep original'), gettext('keep original'),
gettext('override new'), gettext('override new'),

View File

@ -68,6 +68,12 @@
<div ng-show="users.length"> <div ng-show="users.length">
<h3 translate>Preview</h3> <h3 translate>Preview</h3>
<div class="clearfix" ng-if="users.length > pagination.itemsPerPage">
<span class="pull-right">
<translate>Page</translate> {{ pagination.currentPage }} /
{{ Math.ceil(users.length/pagination.itemsPerPage) }}
</span>
</div>
<div class="scroll-x-container"> <div class="scroll-x-container">
<table class="table table-striped table-bordered table-condensed"> <table class="table table-striped table-bordered table-condensed">
<thead> <thead>
@ -106,7 +112,7 @@
</div> </div>
</th> </th>
<tbody> <tbody>
<tr ng-repeat="user in users | limitTo : itemsPerPage : limitBegin"> <tr ng-repeat="user in users | limitTo : pagination.itemsPerPage : pagination.limitBegin">
<td class="minimum" <td class="minimum"
ng-class="{ 'text-danger': (user.importerror || user.duplicateAction == duplicateActions[0]), 'text-success': user.imported }"> ng-class="{ 'text-danger': (user.importerror || user.duplicateAction == duplicateActions[0]), 'text-success': user.imported }">
<span ng-if="user.importerror || user.duplicateAction == duplicateActions[0]"> <span ng-if="user.importerror || user.duplicateAction == duplicateActions[0]">
@ -120,7 +126,7 @@
<i class="fa fa-check-circle fa-lg"></i> <i class="fa fa-check-circle fa-lg"></i>
</span> </span>
<td class="nobr"> <td class="nobr">
{{ (currentPage - 1) * itemsPerPage + $index + 1 }} {{ (pagination.currentPage - 1) * pagination.itemsPerPage + $index + 1 }}
<td> <td>
{{ user.title }} {{ user.title }}
<td ng-class="{ 'text-danger': user.name_error }"> <td ng-class="{ 'text-danger': user.name_error }">
@ -180,11 +186,11 @@
</table> </table>
</div> </div>
<ul uib-pagination <ul uib-pagination
ng-show="users.length > itemsPerPage" ng-show="users.length > pagination.itemsPerPage"
total-items="users.length" total-items="users.length"
items-per-page="itemsPerPage" items-per-page="pagination.itemsPerPage"
ng-model="currentPage" ng-model="pagination.currentPage"
ng-change="pageChanged()" ng-change="pagination.pageChanged()"
class="pagination-sm" class="pagination-sm"
direction-links="false" direction-links="false"
boundary-links="true" boundary-links="true"

View File

@ -147,9 +147,10 @@
{{ users.length }} {{ "participants" | translate }}<span ng-if="(users|filter:{selected:true}).length > 0">, {{ users.length }} {{ "participants" | translate }}<span ng-if="(users|filter:{selected:true}).length > 0">,
{{(users|filter:{selected:true}).length}} {{ "selected" | translate }}</span> {{(users|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
</div> </div>
<div class="col-md-6" ng-show="usersFiltered.length > itemsPerPage"> <div class="col-md-6" ng-show="usersFiltered.length > pagination.itemsPerPage">
<span class="pull-right"> <span class="pull-right">
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(usersFiltered.length/itemsPerPage) }} <translate>Page</translate> {{ pagination.currentPage }} /
{{ Math.ceil(usersFiltered.length/pagination.itemsPerPage) }}
</span> </span>
</div> </div>
</div> </div>
@ -304,7 +305,7 @@
ng-mouseleave="user.hover=false" ng-mouseleave="user.hover=false"
ng-class="{'projected': user.isProjected().length}" ng-class="{'projected': user.isProjected().length}"
ng-repeat="user in usersFiltered ng-repeat="user in usersFiltered
| limitTo : itemsPerPage : limitBegin"> | limitTo : pagination.itemsPerPage : pagination.limitBegin">
<!-- select column --> <!-- select column -->
<div ng-show="isSelectMode" os-perms="users.can_manage" class="col-xs-1 centered"> <div ng-show="isSelectMode" os-perms="users.can_manage" class="col-xs-1 centered">
@ -451,11 +452,11 @@
</div><!-- end os-table --> </div><!-- end os-table -->
<ul uib-pagination <ul uib-pagination
ng-show="usersFiltered.length > itemsPerPage" ng-show="usersFiltered.length > pagination.itemsPerPage"
total-items="usersFiltered.length" total-items="usersFiltered.length"
items-per-page="itemsPerPage" items-per-page="pagination.itemsPerPage"
ng-model="currentPage" ng-model="pagination.currentPage"
ng-change="pageChanged()" ng-change="pagination.pageChanged()"
class="pagination-sm" class="pagination-sm"
direction-links="false" direction-links="false"
boundary-links="true" boundary-links="true"

View File

@ -1,3 +1,4 @@
import base64
from typing import Any, Dict, List # noqa from typing import Any, Dict, List # noqa
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
@ -67,8 +68,20 @@ class TemplateView(View):
raise ImproperlyConfigured("'template_name' is not provided.") raise ImproperlyConfigured("'template_name' is not provided.")
if self.template_name not in self.state: if self.template_name not in self.state:
with open(finders.find(self.template_name)) as template: self.state[self.template_name] = self.load_template()
self.state[self.template_name] = template.read()
def load_template(self) -> str:
with open(finders.find(self.template_name)) as template:
return template.read()
def get(self, *args: Any, **kwargs: Any) -> HttpResponse: def get(self, *args: Any, **kwargs: Any) -> HttpResponse:
return HttpResponse(self.state[self.template_name]) return HttpResponse(self.state[self.template_name])
class BinaryTemplateView(TemplateView):
"""
Loads the specified binary template and encode it with base64.
"""
def load_template(self):
with open(finders.find(self.template_name), 'rb') as template:
return base64.b64encode(template.read())

View File

@ -8,11 +8,12 @@ from rest_framework import status
from rest_framework.test import APIClient from rest_framework.test import APIClient
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Tag from openslides.core.models import ConfigStore, Tag
from openslides.motions.models import ( from openslides.motions.models import (
Category, Category,
Motion, Motion,
MotionBlock, MotionBlock,
MotionLog,
State, State,
Workflow, Workflow,
) )
@ -604,6 +605,35 @@ class UpdateMotion(TestCase):
motion = Motion.objects.get() motion = Motion.objects.get()
self.assertEqual(motion.versions.count(), 1) self.assertEqual(motion.versions.count(), 1)
def test_update_comment_creates_log_entry(self):
field_name = 'comment_field_name_texl2i7%sookqerpl29a'
config['motions_comments'] = {
'1': {
'name': field_name,
'public': False
}
}
# Update Config cache
CollectionElement.from_instance(
ConfigStore.objects.get(key='motions_comments')
)
response = self.client.patch(
reverse('motion-detail', args=[self.motion.pk]),
{'title': 'title_test_sfdAaufd56HR7sd5FDq7av',
'text': 'text_test_fiuhefF86()ew1Ef346AF6W',
'comments': {'1': 'comment1_sdpoiuffo3%7dwDwW)'}
},
format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
motion_logs = MotionLog.objects.filter(motion=self.motion)
self.assertEqual(motion_logs.count(), 2)
motion_log = motion_logs.order_by('-time').first()
self.assertTrue(field_name in motion_log.message_list[0])
class DeleteMotion(TestCase): class DeleteMotion(TestCase):
""" """