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:
parent
df523ce526
commit
3ba39c37c1
@ -14,6 +14,7 @@ Agenda:
|
||||
- New permission for managing lists of speakers [#3366].
|
||||
- Fixed multiple request on creation of agenda related items [#3341].
|
||||
- Added possibility to mark speakers [#3570].
|
||||
- New DOCX export of agenda [#3569].
|
||||
|
||||
Motions:
|
||||
- New export dialog [#3185].
|
||||
@ -56,6 +57,8 @@ Motions:
|
||||
- Combined all boolean filters into one dropdown menu and added a filter
|
||||
for amendments [#3501].
|
||||
- Allow to delete own motions [#3516].
|
||||
- Log which comment was updated [#3569].
|
||||
- Save pagination sate to session storage [#3569].
|
||||
|
||||
Elections:
|
||||
- Added pagination for list view [#3393].
|
||||
|
84
openslides/agenda/static/js/agenda/docx.js
Normal file
84
openslides/agenda/static/js/agenda/docx.js
Normal 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);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
||||
})();
|
@ -7,6 +7,7 @@ angular.module('OpenSlidesApp.agenda.site', [
|
||||
'OpenSlidesApp.core.pdf',
|
||||
'OpenSlidesApp.agenda.pdf',
|
||||
'OpenSlidesApp.agenda.csv',
|
||||
'OpenSlidesApp.agenda.docx',
|
||||
])
|
||||
|
||||
.config([
|
||||
@ -100,12 +101,14 @@ angular.module('OpenSlidesApp.agenda.site', [
|
||||
'gettextCatalog',
|
||||
'gettext',
|
||||
'osTableFilter',
|
||||
'osTablePagination',
|
||||
'AgendaCsvExport',
|
||||
'AgendaPdfExport',
|
||||
'AgendaDocxExport',
|
||||
'ErrorMessage',
|
||||
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm,
|
||||
AgendaTree, Projector, ProjectionDefault, gettextCatalog, gettext, osTableFilter,
|
||||
AgendaCsvExport, AgendaPdfExport, ErrorMessage) {
|
||||
osTablePagination, AgendaCsvExport, AgendaPdfExport, AgendaDocxExport, ErrorMessage) {
|
||||
// Bind agenda tree to the scope
|
||||
$scope.$watch(function () {
|
||||
return Agenda.lastModified();
|
||||
@ -167,13 +170,7 @@ angular.module('OpenSlidesApp.agenda.site', [
|
||||
};
|
||||
|
||||
// pagination
|
||||
$scope.currentPage = 1;
|
||||
$scope.itemsPerPage = 25;
|
||||
$scope.limitBegin = 0;
|
||||
$scope.pageChanged = function() {
|
||||
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
|
||||
$scope.gotoTop();
|
||||
};
|
||||
$scope.pagination = osTablePagination.createInstance('AgendaTablePagination');
|
||||
|
||||
// parse duration for inline editing
|
||||
$scope.generateDurationText = function (item) {
|
||||
@ -288,6 +285,9 @@ angular.module('OpenSlidesApp.agenda.site', [
|
||||
$scope.csvExport = function () {
|
||||
AgendaCsvExport.export($scope.itemsFiltered);
|
||||
};
|
||||
$scope.docxExport = function () {
|
||||
AgendaDocxExport.export($scope.itemsFiltered);
|
||||
};
|
||||
|
||||
/** select mode functions **/
|
||||
$scope.isSelectMode = false;
|
||||
|
@ -111,6 +111,12 @@
|
||||
CSV
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" ng-click="docxExport()">
|
||||
<i class="fa fa-file-word-o"></i>
|
||||
DOCX
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -143,9 +149,10 @@
|
||||
</span>
|
||||
</span>
|
||||
</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">
|
||||
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(itemsFiltered.length/itemsPerPage) }}
|
||||
<translate>Page</translate> {{ pagination.currentPage }} /
|
||||
{{ Math.ceil(itemsFiltered.length/pagination.itemsPerPage) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -225,7 +232,7 @@
|
||||
| osFilter: filter.filterString : filter.getObjectQueryString
|
||||
| filter: {closed: filter.booleanFilters.closed.value}
|
||||
| filter: {is_hidden: filter.booleanFilters.is_hidden.value})
|
||||
| limitTo : itemsPerPage : limitBegin">
|
||||
| limitTo : pagination.itemsPerPage : pagination.limitBegin">
|
||||
|
||||
<!-- select column -->
|
||||
<div ng-show="isSelectMode" os-perms="agenda.can_manage" class="col-xs-1 centered">
|
||||
@ -368,11 +375,11 @@
|
||||
</div> <!-- container -->
|
||||
|
||||
<ul uib-pagination
|
||||
ng-show="itemsFiltered.length > itemsPerPage"
|
||||
ng-show="itemsFiltered.length > pagination.itemsPerPage"
|
||||
total-items="itemsFiltered.length"
|
||||
items-per-page="itemsPerPage"
|
||||
ng-model="currentPage"
|
||||
ng-change="pageChanged()"
|
||||
items-per-page="pagination.itemsPerPage"
|
||||
ng-model="pagination.currentPage"
|
||||
ng-change="pagination.pageChanged()"
|
||||
class="pagination-sm"
|
||||
direction-links="false"
|
||||
boundary-links="true"
|
||||
|
BIN
openslides/agenda/static/templates/docx/agenda.docx
Normal file
BIN
openslides/agenda/static/templates/docx/agenda.docx
Normal file
Binary file not shown.
9
openslides/agenda/urls.py
Normal file
9
openslides/agenda/urls.py
Normal 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'),
|
||||
]
|
@ -15,6 +15,7 @@ from openslides.utils.rest_api import (
|
||||
detail_route,
|
||||
list_route,
|
||||
)
|
||||
from openslides.utils.views import BinaryTemplateView
|
||||
|
||||
from ..utils.auth import has_perm
|
||||
from .access_permissions import ItemAccessPermissions
|
||||
@ -311,3 +312,11 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
|
||||
inform_changed_data(items)
|
||||
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'
|
||||
|
@ -274,11 +274,13 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
'User',
|
||||
'osTableFilter',
|
||||
'osTableSort',
|
||||
'osTablePagination',
|
||||
'gettext',
|
||||
'AssignmentPhases',
|
||||
'AssignmentPdfExport',
|
||||
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, Projector, ProjectionDefault,
|
||||
gettextCatalog, User, osTableFilter, osTableSort, gettext, AssignmentPhases, AssignmentPdfExport) {
|
||||
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, Projector,
|
||||
ProjectionDefault, gettextCatalog, User, osTableFilter, osTableSort, osTablePagination,
|
||||
gettext, AssignmentPhases, AssignmentPdfExport) {
|
||||
$scope.$watch(function () {
|
||||
return Assignment.lastModified();
|
||||
}, function () {
|
||||
@ -354,13 +356,7 @@ angular.module('OpenSlidesApp.assignments.site', [
|
||||
};
|
||||
|
||||
// Pagination
|
||||
$scope.currentPage = 1;
|
||||
$scope.itemsPerPage = 25;
|
||||
$scope.limitBegin = 0;
|
||||
$scope.pageChanged = function() {
|
||||
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
|
||||
$scope.gotoTop();
|
||||
};
|
||||
$scope.pagination = osTablePagination.createInstance('AssignmentTablePagination');
|
||||
|
||||
// update phase
|
||||
$scope.updatePhase = function (assignment, phase_id) {
|
||||
|
@ -77,9 +77,10 @@
|
||||
{{ assignments.length }} {{ "elections" | translate }}<span ng-if="(assignments|filter:{selected:true}).length > 0">,
|
||||
{{(assignments|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
|
||||
</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">
|
||||
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(assignmentsFiltered.length/itemsPerPage) }}
|
||||
<translate>Page</translate> {{ pagination.currentPage }} /
|
||||
{{ Math.ceil(assignmentsFiltered.length/pagination.itemsPerPage) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -87,7 +88,7 @@
|
||||
<div class="os-table container-fluid">
|
||||
<div class="row header-row">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-xs-11 main-header">
|
||||
@ -216,7 +217,7 @@
|
||||
| MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag
|
||||
| MultiselectFilter: filter.multiselectFilters.phase : getItemId.phase
|
||||
| orderByEmptyLast: sort.column : sort.reverse)
|
||||
| limitTo : itemsPerPage : limitBegin">
|
||||
| limitTo : pagination.itemsPerPage : pagination.limitBegin">
|
||||
|
||||
<!-- select column -->
|
||||
<div ng-show="isSelectMode" os-perms="assignments.can_manage" class="col-xs-1 centered">
|
||||
@ -332,11 +333,11 @@
|
||||
</div> <!-- main table -->
|
||||
|
||||
<ul uib-pagination
|
||||
ng-show="assignmentsFiltered.length > itemsPerPage"
|
||||
ng-show="assignmentsFiltered.length > pagination.itemsPerPage"
|
||||
total-items="assignmentsFiltered.length"
|
||||
items-per-page="itemsPerPage"
|
||||
ng-model="currentPage"
|
||||
ng-change="pageChanged()"
|
||||
items-per-page="pagination.itemsPerPage"
|
||||
ng-model="pagination.currentPage"
|
||||
ng-change="pagination.pageChanged()"
|
||||
class="pagination-sm"
|
||||
direction-links="false"
|
||||
boundary-links="true"
|
||||
|
@ -76,6 +76,8 @@ class CoreAppConfig(AppConfig):
|
||||
|
||||
# Client settings
|
||||
client_settings_keys = [
|
||||
'MOTION_IDENTIFIER_MIN_DIGITS',
|
||||
'MOTION_IDENTIFIER_WITHOUT_BLANKS',
|
||||
'MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS'
|
||||
]
|
||||
client_settings_dict = {}
|
||||
|
@ -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
|
||||
* also has this dialog open. Use it like in this example in any dialog controller:
|
||||
var editingStoppedCallback = EditingWarning.editingStarted('editing_name' + item.id);
|
||||
|
@ -17,13 +17,14 @@ angular.module('OpenSlidesApp.mediafiles.list', [
|
||||
'ngDialog',
|
||||
'osTableFilter',
|
||||
'osTableSort',
|
||||
'osTablePagination',
|
||||
'ProjectionDefault',
|
||||
'Projector',
|
||||
'User',
|
||||
'Mediafile',
|
||||
'MediafileForm',
|
||||
'Logos',
|
||||
function ($http, $scope, gettext, ngDialog, osTableFilter, osTableSort,
|
||||
function ($http, $scope, gettext, ngDialog, osTableFilter, osTableSort, osTablePagination,
|
||||
ProjectionDefault, Projector, User, Mediafile, MediafileForm, Logos) {
|
||||
$scope.$watch(function () {
|
||||
return Mediafile.lastModified();
|
||||
@ -107,13 +108,7 @@ angular.module('OpenSlidesApp.mediafiles.list', [
|
||||
];
|
||||
|
||||
// pagination
|
||||
$scope.currentPage = 1;
|
||||
$scope.itemsPerPage = 25;
|
||||
$scope.limitBegin = 0;
|
||||
$scope.pageChanged = function() {
|
||||
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
|
||||
$scope.gotoTop();
|
||||
};
|
||||
$scope.pagination = osTablePagination.createInstance('MediafileTablePagination');
|
||||
|
||||
// open new/edit dialog
|
||||
$scope.openDialog = function (mediafile) {
|
||||
|
@ -144,9 +144,10 @@
|
||||
{{ mediafiles.length }} {{ "files" | translate }}<span ng-if="(mediafiles|filter:{selected:true}).length > 0">,
|
||||
{{(mediafiles|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
|
||||
</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">
|
||||
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(mediafilesFiltered.length/itemsPerPage) }}
|
||||
<translate>Page</translate> {{ pagination.currentPage }} /
|
||||
{{ Math.ceil(mediafilesFiltered.length/pagination.itemsPerPage) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -248,7 +249,7 @@
|
||||
| filter: {filetype: (filter.booleanFilters.isPdf.value ? 'application/pdf' : (filter.booleanFilters.isPdf.value === false ? '!application/pdf' : ''))}
|
||||
| filter: {hidden: filter.booleanFilters.isHidden.value}
|
||||
| orderByEmptyLast: sort.column : sort.reverse)
|
||||
| limitTo: itemsPerPage : limitBegin">
|
||||
| limitTo: pagination.itemsPerPage : pagination.limitBegin">
|
||||
|
||||
<!-- select column -->
|
||||
<div ng-show="isSelectMode" os-perms="mediafiles.can_manage" class="col-xs-1 centered">
|
||||
@ -368,11 +369,11 @@
|
||||
</div><!-- end os-table -->
|
||||
|
||||
<ul uib-pagination
|
||||
ng-show="mediafilesFiltered.length > itemsPerPage"
|
||||
ng-show="mediafilesFiltered.length > pagination.itemsPerPage"
|
||||
total-items="mediafilesFiltered.length"
|
||||
items-per-page="itemsPerPage"
|
||||
ng-model="currentPage"
|
||||
ng-change="pageChanged()"
|
||||
items-per-page="pagination.itemsPerPage"
|
||||
ng-model="pagination.currentPage"
|
||||
ng-change="pagination.pageChanged()"
|
||||
class="pagination-sm"
|
||||
direction-links="false"
|
||||
boundary-links="true"
|
||||
|
@ -272,6 +272,14 @@ angular.module('OpenSlidesApp.motions', [
|
||||
title += this.getTitle();
|
||||
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) {
|
||||
return this.getVersion(versionId).text;
|
||||
},
|
||||
|
@ -16,8 +16,9 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
'Config',
|
||||
'Motion',
|
||||
'MotionComment',
|
||||
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter, HTMLValidizer,
|
||||
Category, Config, Motion, MotionComment) {
|
||||
'OpenSlidesSettings',
|
||||
function($q, operator, gettextCatalog, PDFLayout, PdfMakeConverter, ImageConverter,
|
||||
HTMLValidizer, Category, Config, Motion, MotionComment, OpenSlidesSettings) {
|
||||
/**
|
||||
* Provides the content as JS objects for Motions in pdfMake context
|
||||
* @constructor
|
||||
@ -77,7 +78,8 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
);
|
||||
}
|
||||
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);
|
||||
|
||||
|
@ -962,13 +962,14 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'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, MotionExportForm, MotionPdfExport,
|
||||
PersonalNoteManager) {
|
||||
ProjectionDefault, osTableFilter, osTableSort, osTablePagination, MotionExportForm,
|
||||
MotionPdfExport, PersonalNoteManager) {
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
MotionBlock.bindAll({}, $scope, 'motionBlocks');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
@ -1177,14 +1178,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
];
|
||||
|
||||
// pagination
|
||||
$scope.currentPage = 1;
|
||||
$scope.itemsPerPage = 25;
|
||||
$scope.limitBegin = 0;
|
||||
$scope.pageChanged = function() {
|
||||
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
|
||||
$scope.gotoTop();
|
||||
};
|
||||
|
||||
$scope.pagination = osTablePagination.createInstance('MotionTablePagination');
|
||||
|
||||
// update state
|
||||
$scope.updateState = function (motion, state_id) {
|
||||
@ -1497,6 +1491,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
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);
|
||||
},
|
||||
|
@ -78,10 +78,13 @@
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<translate>This version is not permitted.</translate>
|
||||
</span>
|
||||
<small>
|
||||
<translate>Sequential number</translate> {{ motion.getSequentialNumber() }}
|
||||
</small>
|
||||
</h2>
|
||||
</div>
|
||||
<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})"
|
||||
class="btn btn-default btn-sm" ng-disabled="!navigation.previousMotion">
|
||||
<i class="fa fa-angle-double-left"></i>
|
||||
|
@ -126,9 +126,10 @@
|
||||
{{ motions.length }} {{ "motions" | translate }}<span ng-if="(motions|filter:{selected:true}).length > 0">,
|
||||
{{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
|
||||
</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">
|
||||
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(motionsFiltered.length/itemsPerPage) }}
|
||||
<translate>Page</translate> {{ pagination.currentPage }} /
|
||||
{{ Math.ceil(motionsFiltered.length/pagination.itemsPerPage) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -532,7 +533,7 @@
|
||||
| filter: {isAmendment: filter.booleanFilters.isAmendment.value}
|
||||
| toArray
|
||||
| orderByEmptyLast: sort.column : sort.reverse)
|
||||
| limitTo : itemsPerPage : limitBegin">
|
||||
| limitTo : pagination.itemsPerPage : pagination.limitBegin">
|
||||
|
||||
<!-- select column -->
|
||||
<div ng-show="isSelectMode" os-perms="motions.can_manage" class="col-xs-1 centered">
|
||||
@ -788,11 +789,11 @@
|
||||
</div> <!-- data row -->
|
||||
|
||||
<ul uib-pagination
|
||||
ng-show="motionsFiltered.length > itemsPerPage"
|
||||
ng-show="motionsFiltered.length > pagination.itemsPerPage"
|
||||
total-items="motionsFiltered.length"
|
||||
items-per-page="itemsPerPage"
|
||||
ng-model="currentPage"
|
||||
ng-change="pageChanged()"
|
||||
items-per-page="pagination.itemsPerPage"
|
||||
ng-model="pagination.currentPage"
|
||||
ng-change="pagination.pageChanged()"
|
||||
class="pagination-sm"
|
||||
direction-links="false"
|
||||
boundary-links="true"
|
||||
|
@ -1,9 +1,7 @@
|
||||
import base64
|
||||
import re
|
||||
from typing import Optional # noqa
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.http import Http404
|
||||
@ -24,7 +22,7 @@ from ..utils.rest_api import (
|
||||
ValidationError,
|
||||
detail_route,
|
||||
)
|
||||
from ..utils.views import APIView
|
||||
from ..utils.views import BinaryTemplateView
|
||||
from .access_permissions import (
|
||||
CategoryAccessPermissions,
|
||||
MotionAccessPermissions,
|
||||
@ -206,6 +204,16 @@ class MotionViewSet(ModelViewSet):
|
||||
# No comments here. Just do nothing.
|
||||
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.
|
||||
serializer = self.get_serializer(
|
||||
motion,
|
||||
@ -222,6 +230,11 @@ class MotionViewSet(ModelViewSet):
|
||||
updated_motion.supporters.clear()
|
||||
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
|
||||
# without permission to see users may not have them but can get it now.
|
||||
new_users = list(updated_motion.submitters.all())
|
||||
@ -707,15 +720,10 @@ class WorkflowViewSet(ModelViewSet):
|
||||
return result
|
||||
|
||||
|
||||
# Special API views
|
||||
# Special views
|
||||
|
||||
class MotionDocxTemplateView(APIView):
|
||||
class MotionDocxTemplateView(BinaryTemplateView):
|
||||
"""
|
||||
Returns the template for motions docx export
|
||||
"""
|
||||
http_method_names = ['get']
|
||||
|
||||
def get_context_data(self, **context):
|
||||
with open(finders.find('templates/docx/motions.docx'), "rb") as file:
|
||||
response = base64.b64encode(file.read())
|
||||
return response
|
||||
template_name = 'templates/docx/motions.docx'
|
||||
|
@ -12,6 +12,7 @@ urlpatterns += [
|
||||
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'^rest/', include(router.urls)),
|
||||
url(r'^agenda/', include('openslides.agenda.urls')),
|
||||
url(r'^motions/', include('openslides.motions.urls')),
|
||||
url(r'^users/', include('openslides.users.urls')),
|
||||
|
||||
|
@ -551,12 +551,13 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
'UserCsvExport',
|
||||
'osTableFilter',
|
||||
'osTableSort',
|
||||
'osTablePagination',
|
||||
'gettext',
|
||||
'UserPdfExport',
|
||||
'ErrorMessage',
|
||||
function($scope, $state, $http, $q, ngDialog, UserForm, User, Group, PasswordGenerator,
|
||||
Projector, ProjectionDefault, Config, gettextCatalog, UserCsvExport, osTableFilter,
|
||||
osTableSort, gettext, UserPdfExport, ErrorMessage) {
|
||||
osTableSort, osTablePagination, gettext, UserPdfExport, ErrorMessage) {
|
||||
$scope.$watch(function () {
|
||||
return User.lastModified();
|
||||
}, function () {
|
||||
@ -649,13 +650,7 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
];
|
||||
|
||||
// pagination
|
||||
$scope.currentPage = 1;
|
||||
$scope.itemsPerPage = 25;
|
||||
$scope.limitBegin = 0;
|
||||
$scope.pageChanged = function() {
|
||||
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
|
||||
$scope.gotoTop();
|
||||
};
|
||||
$scope.pagination = osTablePagination.createInstance('UserTablePagination');
|
||||
|
||||
// Toggle group from user
|
||||
$scope.toggleGroup = function (user, group) {
|
||||
@ -1121,15 +1116,13 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
'User',
|
||||
'Group',
|
||||
'UserCsvExport',
|
||||
'osTablePagination',
|
||||
'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
|
||||
$scope.importByLine = function () {
|
||||
var usernames = $scope.userlist[0].split("\n");
|
||||
// Ignore empty lines.
|
||||
/*usernames = _.filter(usernames, function (name) {
|
||||
return name !== '';
|
||||
});*/
|
||||
var users = _.map(usernames, function (name) {
|
||||
// Split each full name in first and last name.
|
||||
// The last word is set as last name, rest is the first name(s).
|
||||
@ -1157,12 +1150,9 @@ angular.module('OpenSlidesApp.users.site', [
|
||||
};
|
||||
|
||||
// pagination
|
||||
$scope.currentPage = 1;
|
||||
$scope.itemsPerPage = 100;
|
||||
$scope.limitBegin = 0;
|
||||
$scope.pageChanged = function() {
|
||||
$scope.limitBegin = ($scope.currentPage - 1) * $scope.itemsPerPage;
|
||||
};
|
||||
$scope.pagination = osTablePagination.createInstance('UserImportTablePagination', 100);
|
||||
|
||||
// Duplicates
|
||||
$scope.duplicateActions = [
|
||||
gettext('keep original'),
|
||||
gettext('override new'),
|
||||
|
@ -68,6 +68,12 @@
|
||||
|
||||
<div ng-show="users.length">
|
||||
<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">
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
@ -106,7 +112,7 @@
|
||||
</div>
|
||||
</th>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users | limitTo : itemsPerPage : limitBegin">
|
||||
<tr ng-repeat="user in users | limitTo : pagination.itemsPerPage : pagination.limitBegin">
|
||||
<td class="minimum"
|
||||
ng-class="{ 'text-danger': (user.importerror || user.duplicateAction == duplicateActions[0]), 'text-success': user.imported }">
|
||||
<span ng-if="user.importerror || user.duplicateAction == duplicateActions[0]">
|
||||
@ -120,7 +126,7 @@
|
||||
<i class="fa fa-check-circle fa-lg"></i>
|
||||
</span>
|
||||
<td class="nobr">
|
||||
{{ (currentPage - 1) * itemsPerPage + $index + 1 }}
|
||||
{{ (pagination.currentPage - 1) * pagination.itemsPerPage + $index + 1 }}
|
||||
<td>
|
||||
{{ user.title }}
|
||||
<td ng-class="{ 'text-danger': user.name_error }">
|
||||
@ -180,11 +186,11 @@
|
||||
</table>
|
||||
</div>
|
||||
<ul uib-pagination
|
||||
ng-show="users.length > itemsPerPage"
|
||||
ng-show="users.length > pagination.itemsPerPage"
|
||||
total-items="users.length"
|
||||
items-per-page="itemsPerPage"
|
||||
ng-model="currentPage"
|
||||
ng-change="pageChanged()"
|
||||
items-per-page="pagination.itemsPerPage"
|
||||
ng-model="pagination.currentPage"
|
||||
ng-change="pagination.pageChanged()"
|
||||
class="pagination-sm"
|
||||
direction-links="false"
|
||||
boundary-links="true"
|
||||
|
@ -147,9 +147,10 @@
|
||||
{{ users.length }} {{ "participants" | translate }}<span ng-if="(users|filter:{selected:true}).length > 0">,
|
||||
{{(users|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
|
||||
</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">
|
||||
<translate>Page</translate> {{ currentPage }} / {{ Math.ceil(usersFiltered.length/itemsPerPage) }}
|
||||
<translate>Page</translate> {{ pagination.currentPage }} /
|
||||
{{ Math.ceil(usersFiltered.length/pagination.itemsPerPage) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -304,7 +305,7 @@
|
||||
ng-mouseleave="user.hover=false"
|
||||
ng-class="{'projected': user.isProjected().length}"
|
||||
ng-repeat="user in usersFiltered
|
||||
| limitTo : itemsPerPage : limitBegin">
|
||||
| limitTo : pagination.itemsPerPage : pagination.limitBegin">
|
||||
|
||||
<!-- select column -->
|
||||
<div ng-show="isSelectMode" os-perms="users.can_manage" class="col-xs-1 centered">
|
||||
@ -451,11 +452,11 @@
|
||||
</div><!-- end os-table -->
|
||||
|
||||
<ul uib-pagination
|
||||
ng-show="usersFiltered.length > itemsPerPage"
|
||||
ng-show="usersFiltered.length > pagination.itemsPerPage"
|
||||
total-items="usersFiltered.length"
|
||||
items-per-page="itemsPerPage"
|
||||
ng-model="currentPage"
|
||||
ng-change="pageChanged()"
|
||||
items-per-page="pagination.itemsPerPage"
|
||||
ng-model="pagination.currentPage"
|
||||
ng-change="pagination.pageChanged()"
|
||||
class="pagination-sm"
|
||||
direction-links="false"
|
||||
boundary-links="true"
|
||||
|
@ -1,3 +1,4 @@
|
||||
import base64
|
||||
from typing import Any, Dict, List # noqa
|
||||
|
||||
from django.contrib.staticfiles import finders
|
||||
@ -67,8 +68,20 @@ class TemplateView(View):
|
||||
raise ImproperlyConfigured("'template_name' is not provided.")
|
||||
|
||||
if self.template_name not in self.state:
|
||||
with open(finders.find(self.template_name)) as template:
|
||||
self.state[self.template_name] = template.read()
|
||||
self.state[self.template_name] = self.load_template()
|
||||
|
||||
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:
|
||||
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())
|
||||
|
@ -8,11 +8,12 @@ from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from openslides.core.config import config
|
||||
from openslides.core.models import Tag
|
||||
from openslides.core.models import ConfigStore, Tag
|
||||
from openslides.motions.models import (
|
||||
Category,
|
||||
Motion,
|
||||
MotionBlock,
|
||||
MotionLog,
|
||||
State,
|
||||
Workflow,
|
||||
)
|
||||
@ -604,6 +605,35 @@ class UpdateMotion(TestCase):
|
||||
motion = Motion.objects.get()
|
||||
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):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user