Merge pull request #2783 from FinnStutzenstein/PapaParse
New csv import using PapaParse
This commit is contained in:
commit
cc0c0bf0d4
@ -51,6 +51,7 @@ Motions:
|
|||||||
- New PDF layout.
|
- New PDF layout.
|
||||||
- Added DOCX export with docxtemplater.
|
- Added DOCX export with docxtemplater.
|
||||||
- Changed label of former state "commited a bill" to "refered to committee".
|
- Changed label of former state "commited a bill" to "refered to committee".
|
||||||
|
- New csv import layout and using Papa Parse for parsing the csv.
|
||||||
|
|
||||||
Elections:
|
Elections:
|
||||||
- Added options to calculate percentages on different bases.
|
- Added options to calculate percentages on different bases.
|
||||||
|
@ -222,6 +222,7 @@ OpenSlides uses the following projects or parts of them:
|
|||||||
* `ngStorage <https://github.com/gsklee/ngStorage>`_, License: MIT
|
* `ngStorage <https://github.com/gsklee/ngStorage>`_, License: MIT
|
||||||
* `ngbootbox <https://github.com/eriktufvesson/ngBootbox>`_, License: MIT
|
* `ngbootbox <https://github.com/eriktufvesson/ngBootbox>`_, License: MIT
|
||||||
* `open-sans-fontface <https://github.com/FontFaceKit/open-sans>`_, License: Apache License version 2.0
|
* `open-sans-fontface <https://github.com/FontFaceKit/open-sans>`_, License: Apache License version 2.0
|
||||||
|
* `Papa Parse <http://papaparse.com/>`_, License: MIT
|
||||||
* `pdfjs-dist <http://mozilla.github.io/pdf.js/>`_, License: Apache-2.0
|
* `pdfjs-dist <http://mozilla.github.io/pdf.js/>`_, License: Apache-2.0
|
||||||
* `roboto-condensed <https://github.com/davidcunningham/roboto-condensed>`_, License: Apache 2.0
|
* `roboto-condensed <https://github.com/davidcunningham/roboto-condensed>`_, License: Apache 2.0
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
"ng-file-upload": "~11.2.3",
|
"ng-file-upload": "~11.2.3",
|
||||||
"ngstorage": "~0.3.11",
|
"ngstorage": "~0.3.11",
|
||||||
"ngBootbox": "~0.1.3",
|
"ngBootbox": "~0.1.3",
|
||||||
|
"papaparse": "~4.1.2",
|
||||||
"pdfmake": "~0.1.23",
|
"pdfmake": "~0.1.23",
|
||||||
"roboto-fontface": "~0.6.0"
|
"roboto-fontface": "~0.6.0"
|
||||||
},
|
},
|
||||||
|
@ -1453,22 +1453,31 @@ img {
|
|||||||
margin-right: .2em;
|
margin-right: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* for angular-csv-import form */
|
/* for csv import form */
|
||||||
.import {
|
.import {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
|
width: 35%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import .label {
|
.import .file-select input {
|
||||||
color: #333 !important;
|
display: none;
|
||||||
font-size: 100%;
|
}
|
||||||
|
|
||||||
|
.import .file-select label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
width: 100px;
|
cursor: pointer;
|
||||||
text-align: left;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.import .label::after {
|
.import .clear-file {
|
||||||
content: ': ';
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import .help-block {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import .help-block-big {
|
||||||
|
font-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* voting results */
|
/* voting results */
|
||||||
|
@ -661,6 +661,84 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/* This directive provides a csv import template.
|
||||||
|
* Papa Parse is used to parse the csv file. Accepted attributes:
|
||||||
|
* * change:
|
||||||
|
* Callback if file changes. The one parameter is csv passing the parsed file
|
||||||
|
* * config (optional):
|
||||||
|
* - accept: String with extensions: default '.csv .txt'
|
||||||
|
* - encodingOptions: List with encodings. Default ['UTF-8', 'ISO-8859-1']
|
||||||
|
* - parseConfig: a dict passed to PapaParse
|
||||||
|
*/
|
||||||
|
.directive('csvImport', [
|
||||||
|
function () {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: 'static/templates/csv-import.html',
|
||||||
|
scope: {
|
||||||
|
change: '&',
|
||||||
|
config: '=?',
|
||||||
|
},
|
||||||
|
controller: function ($scope, $element, $attrs, $location) {
|
||||||
|
// set config if it is not given
|
||||||
|
if (!$scope.config) {
|
||||||
|
$scope.config = {};
|
||||||
|
}
|
||||||
|
if (!$scope.config.parseConfig) {
|
||||||
|
$scope.config.parseConfig = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.inputElement = angular.element($element[0].querySelector('#csvFileSelector'));
|
||||||
|
|
||||||
|
// set accept and encoding
|
||||||
|
$scope.accept = $scope.config.accept || '.csv';
|
||||||
|
$scope.encodingOptions = $scope.config.encodingOptions || ['UTF-8'];
|
||||||
|
$scope.encoding = $scope.encodingOptions[0];
|
||||||
|
|
||||||
|
$scope.parse = function () {
|
||||||
|
var inputElement = $scope.inputElement[0];
|
||||||
|
if (!inputElement.files.length) {
|
||||||
|
$scope.change({csv: {data: {}}});
|
||||||
|
} else {
|
||||||
|
var parseConfig = _.defaults(_.clone($scope.config.parseConfig), {
|
||||||
|
delimiter: $scope.delimiter,
|
||||||
|
encoding: $scope.encoding,
|
||||||
|
header: false, // we do not want to have dicts in result
|
||||||
|
complete: function (csv) {
|
||||||
|
csv.data = csv.data.splice(1); // do not interpret the header as data
|
||||||
|
$scope.$apply(function () {
|
||||||
|
if (csv.meta.delimiter) {
|
||||||
|
$scope.autodelimiter = csv.meta.delimiter;
|
||||||
|
}
|
||||||
|
$scope.change({csv: csv});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$scope.$apply(function () {
|
||||||
|
$scope.change({csv: {data: {}}});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Papa.parse(inputElement.files[0], parseConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.clearFile = function () {
|
||||||
|
$scope.inputElement[0].value = '';
|
||||||
|
$scope.selectedFile = undefined;
|
||||||
|
$scope.parse();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.inputElement.on('change', function () {
|
||||||
|
$scope.selectedFile = _.last($scope.inputElement[0].value.split('\\'));
|
||||||
|
$scope.parse();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
.controller('MainMenuCtrl', [
|
.controller('MainMenuCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'mainMenu',
|
'mainMenu',
|
||||||
|
30
openslides/core/static/templates/csv-import.html
Normal file
30
openslides/core/static/templates/csv-import.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<form class="import">
|
||||||
|
<label for="fileInput" translate>CSV file</label>
|
||||||
|
<div id="fileInput" class="file-select">
|
||||||
|
<div class="form-control">
|
||||||
|
<label for="csvFileSelector">
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
|
{{ selectedFile || ('Select a file' | translate) }}
|
||||||
|
</label>
|
||||||
|
<a href class="pull-right clear-file" ng-if="selectedFile" ng-click="clearFile()">
|
||||||
|
<i class="fa fa-times" title="{{ 'Deselect file' | translate }}"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<input id="csvFileSelector" type="file" value="" accept="{{ accept }}">
|
||||||
|
<p class="help-block">
|
||||||
|
<translate translate-context="special filetypes in a file open dialog">Accept</translate>: {{ accept }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="selectEncoding" translate>Encoding</label>
|
||||||
|
<select class="form-control" ng-model="$parent.encoding" ng-if="encodingOptions.length > 1" ng-change="parse()"
|
||||||
|
id="selectEncoding" ng-options="enc for enc in encodingOptions"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputDelimiter" translate>Separator</label>
|
||||||
|
<input type="text" class="form-control" ng-model="delimiter" ng-change="parse()" id="inputDelimiter">
|
||||||
|
<p class="help-block help-block-big" ng-if="autodelimiter"><translate>Autodetect</translate>: {{ autodelimiter }}</p>
|
||||||
|
<p class="help-block" ng-if="!autodelimiter" translate>Leave empty for autodetection of the separator.</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -1607,73 +1607,56 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'Category',
|
'Category',
|
||||||
'Motion',
|
'Motion',
|
||||||
'User',
|
'User',
|
||||||
function($scope, $q, gettext, Category, Motion, User) {
|
'gettextCatalog',
|
||||||
|
function($scope, $q, gettext, Category, Motion, User, gettextCatalog) {
|
||||||
// set initial data for csv import
|
// set initial data for csv import
|
||||||
$scope.motions = [];
|
$scope.motions = [];
|
||||||
$scope.separator = ',';
|
|
||||||
$scope.encoding = 'UTF-8';
|
// set csv
|
||||||
$scope.encodingOptions = ['UTF-8', 'ISO-8859-1'];
|
$scope.csvConfig = {
|
||||||
$scope.accept = '.csv, .txt';
|
accept: '.csv, .txt',
|
||||||
$scope.csv = {
|
encodingOptions: ['UTF-8', 'ISO-8859-1'],
|
||||||
content: null,
|
parseConfig: {
|
||||||
header: true,
|
skipEmptyLines: true,
|
||||||
headerVisible: false,
|
},
|
||||||
separator: $scope.separator,
|
|
||||||
separatorVisible: false,
|
|
||||||
encoding: $scope.encoding,
|
|
||||||
encodingVisible: false,
|
|
||||||
accept: $scope.accept,
|
|
||||||
result: null
|
|
||||||
};
|
};
|
||||||
// set csv file encoding
|
|
||||||
$scope.setEncoding = function () {
|
var FIELDS = ['identifier', 'title', 'text', 'reason', 'submitter', 'category', 'origin'];
|
||||||
$scope.csv.encoding = $scope.encoding;
|
|
||||||
};
|
|
||||||
// set csv file encoding
|
|
||||||
$scope.setSeparator = function () {
|
|
||||||
$scope.csv.separator = $scope.separator;
|
|
||||||
};
|
|
||||||
// detect if csv file is loaded
|
|
||||||
$scope.$watch('csv.result', function () {
|
|
||||||
$scope.motions = [];
|
$scope.motions = [];
|
||||||
var quotionRe = /^"(.*)"$/;
|
$scope.onCsvChange = function (csv) {
|
||||||
angular.forEach($scope.csv.result, function (motion) {
|
$scope.motions = [];
|
||||||
if (motion.identifier) {
|
var motions = [];
|
||||||
motion.identifier = motion.identifier.replace(quotionRe, '$1');
|
_.forEach(csv.data, function (row) {
|
||||||
|
if (row.length >= 3) {
|
||||||
|
var filledRow = _.zipObject(FIELDS, row);
|
||||||
|
motions.push(filledRow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_.forEach(motions, function (motion) {
|
||||||
|
// identifier
|
||||||
if (motion.identifier !== '') {
|
if (motion.identifier !== '') {
|
||||||
// All motion objects are already loaded via the resolve statement from ui-router.
|
// All motion objects are already loaded via the resolve statement from ui-router.
|
||||||
var motions = Motion.getAll();
|
var motions = Motion.getAll();
|
||||||
if (_.find(motions, function (item) {
|
if (_.find(motions, function (item) {
|
||||||
return item.identifier == motion.identifier;
|
return item.identifier === motion.identifier;
|
||||||
})) {
|
})) {
|
||||||
motion.importerror = true;
|
motion.importerror = true;
|
||||||
motion.identifier_error = gettext('Error: Identifier already exists.');
|
motion.identifier_error = gettext('Error: Identifier already exists.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// title
|
// title
|
||||||
if (motion.title) {
|
|
||||||
motion.title = motion.title.replace(quotionRe, '$1');
|
|
||||||
}
|
|
||||||
if (!motion.title) {
|
if (!motion.title) {
|
||||||
motion.importerror = true;
|
motion.importerror = true;
|
||||||
motion.title_error = gettext('Error: Title is required.');
|
motion.title_error = gettext('Error: Title is required.');
|
||||||
}
|
}
|
||||||
// text
|
// text
|
||||||
if (motion.text) {
|
|
||||||
motion.text = motion.text.replace(quotionRe, '$1');
|
|
||||||
}
|
|
||||||
if (!motion.text) {
|
if (!motion.text) {
|
||||||
motion.importerror = true;
|
motion.importerror = true;
|
||||||
motion.text_error = gettext('Error: Text is required.');
|
motion.text_error = gettext('Error: Text is required.');
|
||||||
}
|
}
|
||||||
// reason
|
|
||||||
if (motion.reason) {
|
|
||||||
motion.reason = motion.reason.replace(quotionRe, '$1');
|
|
||||||
}
|
|
||||||
// submitter
|
// submitter
|
||||||
if (motion.submitter) {
|
if (motion.submitter) {
|
||||||
motion.submitter = motion.submitter.replace(quotionRe, '$1');
|
|
||||||
if (motion.submitter !== '') {
|
if (motion.submitter !== '') {
|
||||||
// All user objects are already loaded via the resolve statement from ui-router.
|
// All user objects are already loaded via the resolve statement from ui-router.
|
||||||
var users = User.getAll();
|
var users = User.getAll();
|
||||||
@ -1690,7 +1673,6 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
}
|
}
|
||||||
// category
|
// category
|
||||||
if (motion.category) {
|
if (motion.category) {
|
||||||
motion.category = motion.category.replace(quotionRe, '$1');
|
|
||||||
if (motion.category !== '') {
|
if (motion.category !== '') {
|
||||||
// All categore objects are already loaded via the resolve statement from ui-router.
|
// All categore objects are already loaded via the resolve statement from ui-router.
|
||||||
var categories = Category.getAll();
|
var categories = Category.getAll();
|
||||||
@ -1706,13 +1688,10 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
if (motion.category && motion.category !== '' && !motion.category_id) {
|
if (motion.category && motion.category !== '' && !motion.category_id) {
|
||||||
motion.category_create = gettext('New category will be created.');
|
motion.category_create = gettext('New category will be created.');
|
||||||
}
|
}
|
||||||
// origin
|
|
||||||
if (motion.origin) {
|
|
||||||
motion.origin = motion.origin.replace(quotionRe, '$1');
|
|
||||||
}
|
|
||||||
$scope.motions.push(motion);
|
$scope.motions.push(motion);
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
// Counter for creations
|
// Counter for creations
|
||||||
$scope.usersCreated = 0;
|
$scope.usersCreated = 0;
|
||||||
@ -1824,14 +1803,16 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
$scope.csvimported = true;
|
$scope.csvimported = true;
|
||||||
};
|
};
|
||||||
$scope.clear = function () {
|
$scope.clear = function () {
|
||||||
$scope.csv.result = null;
|
$scope.motions = [];
|
||||||
};
|
};
|
||||||
// download CSV example file
|
// download CSV example file
|
||||||
$scope.downloadCSVExample = function () {
|
$scope.downloadCSVExample = function () {
|
||||||
|
var headerline = ['Identifier', 'Title', 'Text', 'Reason', 'Submitter', 'Category', 'Origin'];
|
||||||
|
headerline = _.map(headerline, function (entry) {
|
||||||
|
return gettextCatalog.getString(entry);
|
||||||
|
});
|
||||||
var element = document.getElementById('downloadLink');
|
var element = document.getElementById('downloadLink');
|
||||||
var csvRows = [
|
var csvRows = [headerline,
|
||||||
// column header line
|
|
||||||
['identifier', 'title', 'text', 'reason', 'submitter', 'category', 'origin'],
|
|
||||||
// example entries
|
// example entries
|
||||||
['A1', 'Title 1', 'Text 1', 'Reason 1', 'Submitter A', 'Category A', 'Last Year Conference A'],
|
['A1', 'Title 1', 'Text 1', 'Reason 1', 'Submitter A', 'Category A', 'Last Year Conference A'],
|
||||||
['B1', 'Title 2', 'Text 2', 'Reason 2', 'Submitter B', 'Category B', '' ],
|
['B1', 'Title 2', 'Text 2', 'Reason 2', 'Submitter B', 'Category B', '' ],
|
||||||
|
@ -12,40 +12,27 @@
|
|||||||
|
|
||||||
<div class="details">
|
<div class="details">
|
||||||
|
|
||||||
<div class="block row">
|
<h3 translate>Select a CSV file</h3>
|
||||||
<div class="title">
|
<csv-import change="onCsvChange(csv)" config="csvConfig"></csv-import>
|
||||||
<h3 translate>Select a CSV file
|
|
||||||
</div>
|
|
||||||
<div class="block right import">
|
|
||||||
<label class="label" for="inputSeparator" translate>Separator</label>
|
|
||||||
<input type="text" ng-model="separator" ng-change="setSeparator()" ng-init="separator=separator" id="inputSeparator">
|
|
||||||
<br>
|
|
||||||
<label class="label" for="selectEncoding" translate>Encoding</label>
|
|
||||||
<select ng-model="encoding" ng-options="enc as enc for enc in encodingOptions"
|
|
||||||
ng-selected="setEncoding()" ng-init="encoding=encoding" id="selectEncoding"></select>
|
|
||||||
<ng-csv-import
|
|
||||||
content="csv.content"
|
|
||||||
header="csv.header"
|
|
||||||
header-visible="csv.headerVisible"
|
|
||||||
separator="csv.separator"
|
|
||||||
separator-visible="csv.separatorVisible"
|
|
||||||
result="csv.result"
|
|
||||||
encoding="csv.encoding"
|
|
||||||
accept="csv.accept"
|
|
||||||
encoding-visible="csv.encodingVisible"></ng-csv-import>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 translate>Please note:</h4>
|
<h4 translate>Please note:</h4>
|
||||||
<ul class="indentation">
|
<ul class="indentation">
|
||||||
<li><translate>Required comma or semicolon separated values with these column header names in the first row</translate>:<br>
|
<li><translate>Required comma or semicolon separated values with these column header names in the first row</translate>:<br>
|
||||||
<code>identifier, title, text, reason, submitter, category, origin</code>
|
<code>
|
||||||
|
<translate>Identifier</translate>,
|
||||||
|
<translate>Title</translate>,
|
||||||
|
<translate>Text</translate>,
|
||||||
|
<translate>Reason</translate>,
|
||||||
|
<translate>Submitter</translate>,
|
||||||
|
<translate>Category</translate>,
|
||||||
|
<translate>Origin</translate>
|
||||||
|
</code>
|
||||||
<li translate>Identifier, reason, submitter, category and origin are optional and may be empty.
|
<li translate>Identifier, reason, submitter, category and origin are optional and may be empty.
|
||||||
<li translate>Only double quotes are accepted as text delimiter (no single quotes).
|
<li translate>Only double quotes are accepted as text delimiter (no single quotes).
|
||||||
<li><a id="downloadLink" href="" ng-click="downloadCSVExample()" translate>Download CSV example file</a>
|
<li><a id="downloadLink" href="" ng-click="downloadCSVExample()" translate>Download CSV example file</a>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div ng-if="csv.result">
|
<div ng-if="motions.length">
|
||||||
<h3 translate>Preview</h3>
|
<h3 translate>Preview</h3>
|
||||||
<table class="table table-striped table-bordered table-condensed">
|
<table class="table table-striped table-bordered table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
|
Loading…
Reference in New Issue
Block a user