Merge pull request #1773 from emanuelschuetze/csv-import

Improved agenda and users csv import (Fixes #1748)
This commit is contained in:
Oskar Hahn 2015-12-12 11:05:03 +01:00
commit 63d4351c9a
5 changed files with 475 additions and 239 deletions

View File

@ -261,14 +261,15 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
.controller('AgendaImportCtrl', [
'$scope',
'gettext',
'Agenda',
'Customslide',
function($scope, Agenda, Customslide) {
function($scope, gettext, Agenda, Customslide) {
// import from textarea
$scope.importByLine = function () {
$scope.items = $scope.itemlist[0].split("\n");
$scope.titleItems = $scope.itemlist[0].split("\n");
$scope.importcounter = 0;
$scope.items.forEach(function(title) {
$scope.titleItems.forEach(function(title) {
var item = {title: title};
// TODO: create all items in bulk mode
Customslide.create(item).then(
@ -279,29 +280,63 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
});
};
// import from csv file
// *** CSV import ***
// set initial data for csv import
$scope.items = []
$scope.separator = ',';
$scope.encoding = 'UTF-8';
$scope.encodingOptions = ['UTF-8', 'ISO-8859-1'];
$scope.csv = {
content: null,
header: true,
separator: ',',
headerVisible: false,
separator: $scope.separator,
separatorVisible: false,
encoding: $scope.encoding,
encodingVisible: false,
result: null
};
$scope.importByCSV = function (result) {
var obj = JSON.parse(JSON.stringify(result));
$scope.csvimporting = true;
$scope.csvlines = Object.keys(obj).length;
$scope.csvimportcounter = 0;
for (var i = 0; i < obj.length; i++) {
var item = {};
item.title = obj[i].title;
item.text = obj[i].text;
// TODO: save also 'duration' in related agenda item
Customslide.create(item).then(
function(success) {
$scope.csvimportcounter++;
}
);
}
// set csv file encoding
$scope.setEncoding = function () {
$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.items = [];
var quotionRe = /^"(.*)"$/;
angular.forEach($scope.csv.result, function (item) {
// title
if (item.title) {
item.title = item.title.replace(quotionRe, '$1');
}
if (!item.title) {
item.importerror = true;
item.title_error = gettext('Error: Title is required.');
}
// text
if (item.text) {
item.text = item.text.replace(quotionRe, '$1');
}
$scope.items.push(item);
});
});
// import from csv file
$scope.import = function () {
$scope.csvImporting = true;
angular.forEach($scope.items, function (item) {
if (!item.importerror) {
Customslide.create(item).then(
function(success) {
item.imported = true;
}
);
}
});
$scope.csvimported = true;
};
$scope.clear = function () {

View File

@ -11,7 +11,7 @@
</div>
<div class="details">
<h3 translate>Import by copy/paste</h3>
<h2 translate>Import by copy/paste</h2>
<p translate>Copy and paste your agenda item titles in this textbox.
Keep each item in a single line.</p>
@ -23,71 +23,121 @@ Keep each item in a single line.</p>
<div class="clearfix">
<button ng-click="importByLine()" class="btn btn-primary pull-left" translate>Import</button>
<div class="col-xs-5" ng-if="items">
<progressbar animate="false" type="success" max="items.length" value="importcounter">
<i>{{ importcounter }} / {{ items.length }} {{ "imported" | translate }}</i>
<div class="col-xs-5" ng-if="titleItems">
<progressbar animate="false" type="success" max="titleItems.length" value="importcounter">
<i>{{ importcounter }} / {{ titleItems.length }} {{ "imported" | translate }}</i>
</progressbar>
</div>
</div>
<div class="spacer">
<a ng-if="importcounter > 0 && importcounter == titleItems.length" ui-sref="agenda.item.list"
class="btn btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to agenda</translate>
</a>
</div>
<hr>
<h3 translate>Import by CSV file</h3>
<p translate>Select a CSV file to import agenda items!
<p translate>Please note:</p>
<ul><!--TODO: utf-8 encoding still required with angular-csv? -->
<li><translate>Required comma separated values</translate>:<br>
<code translate>'title, text'</code>
<li translate>Text and duration are optional and may be empty.
<li translate>The header in first line is required.
<li translate>Required CSV file encoding is UTF-8.
<li><a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import" translate>
Use the CSV example file from OpenSlides Wiki.
</a>
</ul>
<ng-csv-import
content="csv.content"
class="import"
header="csv.header"
separator="csv.separator"
result="csv.result"></ng-csv-import>
<div ng-if="csv.result" class="col-sm-6 well">
<h3 translate>Preview</h3>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th>#
<th translate>Title
<th translate>Text
<th translate>Duration</th>
<tbody ng-repeat="item in csv.result">
<tr>
<td>{{ $index+1 }}
<td>{{ item.title }}
<td>{{ item.text }}
<td>{{ item.duration }}
</table>
<div class="clearfix">
<button ng-if="!csvimporting" ng-click="importByCSV(csv.result)" class="btn btn-primary pull-left" translate>Import</button>
<div ng-if="csvimporting">
<progressbar animate="false" type="success" max="csvlines" value="csvimportcounter">
<i>{{ csvimportcounter }} / {{ csvlines }} {{ "imported" | translate }}</i>
</progressbar>
<h2 translate>Import by CSV file</h2>
<div class="block row">
<div class="title">
<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"
encoding-visible="csv.encodingVisible"></ng-csv-import>
</div>
</div>
<a ng-if="csvimported" ui-sref="agenda.item.list" class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to agenda overview</translate>
</a>
</div>
<p>
<div class="form-group">
<button ng-if="!csvimporting" ng-click="clear()" class="btn btn-default" translate>
Clear
<h4 translate>Please note:</h4>
<ul>
<li><translate>Required comma or semicolon separated values with these column header names in the first row</translate>:<br>
<code>'title, text'</code>
<li translate>Text is optional and may be empty.
<li translate>Only double quotes are accepted as text delimiter (no single quotes).
<li><a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import" translate>
Use the CSV example file from OpenSlides Wiki.
</a>
</ul>
<div ng-if="csv.result">
<h3 translate>Preview</h3>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th>
<th>#
<th translate>Title
<th translate>Text</th>
<tbody ng-repeat="item in items">
<tr>
<td class="minimum"
ng-class="{ 'text-danger': item.importerror, 'text-success': item.imported }">
<span ng-if="item.importerror">
<i class="fa fa-exclamation-triangle fa-lg"></i>
</span>
<span ng-if="!item.importerror && !item.imported">
<i class="fa fa-check-circle-o fa-lg"></i>
</span>
<span ng-if="item.imported">
<i class="fa fa-check-circle fa-lg"></i>
</span>
<td>{{ $index + 1 }}
<td ng-class="{ 'text-danger': item.title_error }">
<span ng-if="item.title_error" title="{{ item.title_error | translate }}">
<i class="fa fa-exclamation-triangle"></i>
</span>
{{ item.title }}
<td>{{ item.text | limitTo:80 }}{{ item.text.length > 80 ? '...' : '' }}
</table>
<div class="text-danger">
<div ng-repeat="item in itemsFailed = (items | filter:{importerror:true})"></div>
<i class="fa fa-exclamation-triangle"></i>
{{ itemsFailed.length }}
<translate>agenda items will be not imported.</translate>
</div>
<div>
<div ng-repeat="item in itemsPassed = (items | filter:{importerror:false})"></div>
<i class="fa fa-check-circle-o fa-lg"></i>
{{ items.length - itemsFailed.length }}
<translate>items will be imported.</translate>
</div>
<div ng-repeat="item in itemsImported = (items | filter:{imported:true})"></div>
<div ng-if="itemsImported.length > 0" class="text-success">
<hr class="smallhr">
<i class="fa fa-check-circle fa-lg"></i>
{{ itemsImported.length }}
<translate>items were successfully imported.</translate>
</div>
<div class="spacer">
<button ng-click="clear()" class="btn btn-default" translate>
Clear preview
</button>
<button ng-if="!csvImporting" ng-click="import()" class="btn btn-primary" translate>
Import {{ items.length - itemsFailed.length }} items
</button>
</div>
</div>
<div class="spacer">
<a ng-if="csvimported" ui-sref="agenda.item.list" class="btn btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to agenda</translate>
</a>
</div>
</div>
</div>

View File

@ -4,14 +4,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
#: users/static/templates/users/user-import.html:41
msgid "'title, first_name, last_name, structure level, groups, comment, is active'"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:41
msgid "'title, text'"
msgstr ""
#: assignments/static/templates/assignments/assignment-list.html:54
msgid "--- Select phase ---"
msgstr ""
@ -20,7 +12,7 @@ msgstr ""
msgid "--- Select state ---"
msgstr ""
#: users/static/js/users/site.js:323
#: users/static/js/users/site.js:348
#: users/static/templates/users/user-detail-profile.html:39
#: users/static/templates/users/user-detail.html:37
msgid "About me"
@ -77,7 +69,7 @@ msgstr ""
msgid "Agenda item"
msgstr ""
#: users/static/templates/users/user-import.html:45
#: users/static/templates/users/user-import.html:74
msgid ""
"At least first name or last name have to be filled in. All\n"
" other fields are optional and may be empty."
@ -88,13 +80,11 @@ msgid "Attachment"
msgstr ""
#: agenda/static/templates/agenda/item-detail.html:6
#: agenda/static/templates/agenda/item-import.html:139
#: agenda/static/templates/agenda/item-import.html:36
msgid "Back to agenda"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:83
msgid "Back to agenda overview"
msgstr ""
#: motions/static/templates/motions/motion-import.html:136
msgid "Back to motions overview"
msgstr ""
@ -118,7 +108,8 @@ msgstr ""
msgid "Back to overview"
msgstr ""
#: users/static/templates/users/user-import.html:93
#: users/static/templates/users/user-import.html:166
#: users/static/templates/users/user-import.html:36
msgid "Back to users overview"
msgstr ""
@ -167,12 +158,9 @@ msgstr ""
msgid "Category"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:88
#: users/static/templates/users/user-import.html:98
msgid "Clear"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:129
#: motions/static/templates/motions/motion-import.html:126
#: users/static/templates/users/user-import.html:156
msgid "Clear preview"
msgstr ""
@ -185,9 +173,9 @@ msgid "Closed"
msgstr ""
#: agenda/static/templates/agenda/item-list.html:174
#: users/static/js/users/site.js:316
#: users/static/js/users/site.js:341
#: users/static/templates/users/user-detail.html:45
#: users/static/templates/users/user-import.html:72
#: users/static/templates/users/user-import.html:94
msgid "Comment"
msgstr ""
@ -233,15 +221,15 @@ msgstr ""
msgid "Default comment on the ballot paper"
msgstr ""
#: users/static/templates/users/user-import.html:42
#: users/static/templates/users/user-import.html:71
msgid "Default groups"
msgstr ""
#: users/static/js/users/site.js:304
#: users/static/js/users/site.js:329
msgid "Default password"
msgstr ""
#: users/static/templates/users/user-import.html:43
#: users/static/templates/users/user-import.html:72
msgid "Delegate"
msgstr ""
@ -287,7 +275,6 @@ msgstr ""
msgid "Drag and drop items to change the order of the agenda. Your modification will be saved immediately."
msgstr ""
#: agenda/static/templates/agenda/item-import.html:65
#: agenda/static/templates/agenda/item-list.html:114
#: agenda/static/templates/agenda/item-list.html:184
msgid "Duration"
@ -379,7 +366,9 @@ msgstr ""
msgid "Elections"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:51
#: motions/static/templates/motions/motion-import.html:23
#: users/static/templates/users/user-import.html:52
msgid "Encoding"
msgstr ""
@ -387,6 +376,10 @@ msgstr ""
msgid "English"
msgstr ""
#: users/static/js/users/site.js:652
msgid "Error: First or last name is required."
msgstr ""
#: motions/static/js/motions/site.js:618
msgid "Error: Identifier already exists."
msgstr ""
@ -395,6 +388,7 @@ msgstr ""
msgid "Error: Text is required."
msgstr ""
#: agenda/static/js/agenda/site.js:319
#: motions/static/js/motions/site.js:628
msgid "Error: Title is required."
msgstr ""
@ -422,9 +416,9 @@ msgstr ""
msgid "Filter"
msgstr ""
#: users/static/js/users/site.js:270
#: users/static/js/users/site.js:295
#: users/static/templates/users/user-detail-profile.html:26
#: users/static/templates/users/user-import.html:68
#: users/static/templates/users/user-import.html:90
msgid "First name"
msgstr ""
@ -440,10 +434,10 @@ msgstr ""
msgid "Group"
msgstr ""
#: users/static/js/users/site.js:291
#: users/static/js/users/site.js:316
#: users/static/templates/users/group-list.html:13
#: users/static/templates/users/user-detail.html:33
#: users/static/templates/users/user-import.html:71
#: users/static/templates/users/user-import.html:93
#: users/static/templates/users/user-list.html:10
#: users/static/templates/users/user-list.html:109
msgid "Groups"
@ -482,11 +476,9 @@ msgid "Identifier, reason, submitter and category are optional and may be empty.
msgstr ""
#: agenda/static/templates/agenda/item-import.html:25
#: agenda/static/templates/agenda/item-import.html:75
#: agenda/static/templates/agenda/item-list.html:14
#: motions/static/templates/motions/motion-list.html:18
#: users/static/templates/users/user-import.html:25
#: users/static/templates/users/user-import.html:85
#: users/static/templates/users/user-list.html:14
msgid "Import"
msgstr ""
@ -495,8 +487,8 @@ msgstr ""
msgid "Import agenda items"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:35
#: users/static/templates/users/user-import.html:35
#: agenda/static/templates/agenda/item-import.html:42
#: users/static/templates/users/user-import.html:42
msgid "Import by CSV file"
msgstr ""
@ -513,15 +505,23 @@ msgstr ""
msgid "Import participants"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:132
msgid "Import {{ items.length - itemsFailed.length }} items"
msgstr ""
#: motions/static/templates/motions/motion-import.html:129
msgid "Import {{ motions.length - motionsFailed.length }} motions"
msgstr ""
#: users/static/js/users/site.js:722
#: users/static/templates/users/user-import.html:159
msgid "Import {{ users.length - usersFailed.length }} participants"
msgstr ""
#: users/static/js/users/site.js:809
msgid "Important: Please change your password!"
msgstr ""
#: users/static/js/users/site.js:720
#: users/static/js/users/site.js:807
msgid "Installation was successfully."
msgstr ""
@ -531,11 +531,12 @@ msgstr ""
msgid "Invalid votes"
msgstr ""
#: users/static/js/users/site.js:338
#: users/static/js/users/site.js:363
#: users/static/templates/users/user-import.html:95
msgid "Is active"
msgstr ""
#: users/static/js/users/site.js:331
#: users/static/js/users/site.js:356
#: users/static/templates/users/user-list.html:70
msgid "Is present"
msgstr ""
@ -544,9 +545,9 @@ msgstr ""
msgid "Item number"
msgstr ""
#: users/static/js/users/site.js:277
#: users/static/js/users/site.js:302
#: users/static/templates/users/user-detail-profile.html:30
#: users/static/templates/users/user-import.html:69
#: users/static/templates/users/user-import.html:91
msgid "Last name"
msgstr ""
@ -692,7 +693,9 @@ msgstr ""
msgid "Old speakers:"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:71
#: motions/static/templates/motions/motion-import.html:43
#: users/static/templates/users/user-import.html:76
msgid "Only double quotes are accepted as text delimiter (no single quotes)."
msgstr ""
@ -743,9 +746,9 @@ msgstr ""
msgid "Phase"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:38
#: agenda/static/templates/agenda/item-import.html:66
#: motions/static/templates/motions/motion-import.html:38
#: users/static/templates/users/user-import.html:38
#: users/static/templates/users/user-import.html:67
msgid "Please note:"
msgstr ""
@ -766,9 +769,9 @@ msgstr ""
msgid "Present"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:58
#: agenda/static/templates/agenda/item-import.html:78
#: motions/static/templates/motions/motion-import.html:50
#: users/static/templates/users/user-import.html:62
#: users/static/templates/users/user-import.html:83
msgid "Preview"
msgstr ""
@ -838,20 +841,12 @@ msgstr ""
msgid "Remove message"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:44
#: users/static/templates/users/user-import.html:48
msgid "Required CSV file encoding is UTF-8."
msgstr ""
#: agenda/static/templates/agenda/item-import.html:68
#: motions/static/templates/motions/motion-import.html:40
#: users/static/templates/users/user-import.html:69
msgid "Required comma or semicolon separated values with these column header names in the first row"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:40
#: users/static/templates/users/user-import.html:40
msgid "Required comma separated values"
msgstr ""
#: core/static/templates/core/projector-controls.html:99
msgid "Reset countdown"
msgstr ""
@ -899,23 +894,17 @@ msgstr ""
msgid "Select"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:45
#: motions/static/templates/motions/motion-import.html:17
#: users/static/templates/users/user-import.html:46
msgid "Select a CSV file"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:36
msgid "Select a CSV file to import agenda items!"
msgstr ""
#: users/static/templates/users/user-import.html:36
msgid "Select a CSV file to import users!"
msgstr ""
#: motions/static/js/motions/site.js:214
msgid "Select or search a category..."
msgstr ""
#: users/static/js/users/site.js:297
#: users/static/js/users/site.js:322
msgid "Select or search a group..."
msgstr ""
@ -947,7 +936,9 @@ msgstr ""
msgid "Select or search an attachment..."
msgstr ""
#: agenda/static/templates/agenda/item-import.html:48
#: motions/static/templates/motions/motion-import.html:20
#: users/static/templates/users/user-import.html:49
msgid "Separator"
msgstr ""
@ -980,7 +971,7 @@ msgstr ""
msgid "Special values"
msgstr ""
#: users/static/templates/users/user-import.html:44
#: users/static/templates/users/user-import.html:73
msgid "Staff"
msgstr ""
@ -1005,10 +996,10 @@ msgstr ""
msgid "Stop current speaker"
msgstr ""
#: users/static/js/users/site.js:284
#: users/static/js/users/site.js:309
#: users/static/templates/users/user-detail-profile.html:35
#: users/static/templates/users/user-detail.html:31
#: users/static/templates/users/user-import.html:70
#: users/static/templates/users/user-import.html:92
#: users/static/templates/users/user-list.html:104
msgid "Structure level"
msgstr ""
@ -1052,7 +1043,7 @@ msgstr ""
msgid "Tags"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:64
#: agenda/static/templates/agenda/item-import.html:85
#: core/static/js/core/site.js:472
#: motions/static/js/motions/site.js:169
#: motions/static/templates/motions/motion-detail.html:232
@ -1060,17 +1051,12 @@ msgstr ""
msgid "Text"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:42
msgid "Text and duration are optional and may be empty."
msgstr ""
#: agenda/static/templates/agenda/item-import.html:43
#: users/static/templates/users/user-import.html:47
msgid "The header in first line is required."
#: agenda/static/templates/agenda/item-import.html:70
msgid "Text is optional and may be empty."
msgstr ""
#. academic degree
#: agenda/static/templates/agenda/item-import.html:63
#: agenda/static/templates/agenda/item-import.html:84
#: agenda/static/templates/agenda/item-list.html:170
#: assignments/static/js/assignments/site.js:77
#: assignments/static/templates/assignments/assignment-list.html:140
@ -1081,9 +1067,9 @@ msgstr ""
#: motions/static/js/motions/site.js:161
#: motions/static/templates/motions/motion-import.html:57
#: motions/static/templates/motions/motion-list.html:95
#: users/static/js/users/site.js:263
#: users/static/js/users/site.js:288
#: users/static/templates/users/user-detail-profile.html:22
#: users/static/templates/users/user-import.html:67
#: users/static/templates/users/user-import.html:89
msgid "Title"
msgstr ""
@ -1109,13 +1095,13 @@ msgstr ""
msgid "Uploaded by"
msgstr ""
#: users/static/js/users/site.js:721
#: users/static/js/users/site.js:808
msgid "Use <strong>admin</strong> and <strong>admin</strong> for first login."
msgstr ""
#: agenda/static/templates/agenda/item-import.html:45
#: agenda/static/templates/agenda/item-import.html:72
#: motions/static/templates/motions/motion-import.html:44
#: users/static/templates/users/user-import.html:49
#: users/static/templates/users/user-import.html:77
msgid "Use the CSV example file from OpenSlides Wiki."
msgstr ""
@ -1125,7 +1111,7 @@ msgstr ""
msgid "Username"
msgstr ""
#: users/static/js/users/site.js:742
#: users/static/js/users/site.js:829
msgid "Username or password was not correct."
msgstr ""
@ -1171,6 +1157,10 @@ msgstr ""
msgid "Yes"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:112
msgid "agenda items will be not imported."
msgstr ""
#: assignments/static/templates/assignments/assignment-list.html:72
msgid "elections"
msgstr ""
@ -1181,9 +1171,7 @@ msgid "h"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:28
#: agenda/static/templates/agenda/item-import.html:78
#: users/static/templates/users/user-import.html:28
#: users/static/templates/users/user-import.html:88
msgid "imported"
msgstr ""
@ -1191,6 +1179,14 @@ msgstr ""
msgid "items"
msgstr ""
#: agenda/static/templates/agenda/item-import.html:125
msgid "items were successfully imported."
msgstr ""
#: agenda/static/templates/agenda/item-import.html:118
msgid "items will be imported."
msgstr ""
#: motions/static/templates/motions/motion-detail.html:117
msgid "majority"
msgstr ""
@ -1215,6 +1211,18 @@ msgstr ""
msgid "participants"
msgstr ""
#: users/static/templates/users/user-import.html:152
msgid "participants were successfully imported."
msgstr ""
#: users/static/templates/users/user-import.html:145
msgid "participants will be imported."
msgstr ""
#: users/static/templates/users/user-import.html:139
msgid "participants will be not imported."
msgstr ""
#: agenda/static/templates/agenda/item-list.html:99
#: assignments/static/templates/assignments/assignment-list.html:73
#: motions/static/templates/motions/motion-list.html:79
@ -1225,3 +1233,7 @@ msgstr ""
#: motions/static/templates/motions/motion-detail.html:118
msgid "undocumented"
msgstr ""
#: users/static/templates/users/user-import.html:127
msgid "{{ groupname }}"
msgstr ""

View File

@ -80,6 +80,11 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
.state('users.user.import', {
url: '/import',
controller: 'UserImportCtrl',
resolve: {
groups: function(Group) {
return Group.findAll();
}
}
})
// groups
.state('users.group', {
@ -375,6 +380,7 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
function($scope, $state, ngDialog, User, Group) {
User.bindAll({}, $scope, 'users');
Group.bindAll({}, $scope, 'groups');
$scope.alert = {};
// setup table sorting
$scope.sortColumn = 'first_name'; //TODO: sort by first OR last name
@ -411,7 +417,7 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
};
// save changed user
$scope.save = function (user) {
Assignment.save(user).then(
User.save(user).then(
function(success) {
//user.quickEdit = false;
$scope.alert.show = false;
@ -575,13 +581,15 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
.controller('UserImportCtrl', [
'$scope',
'gettext',
'User',
function($scope, User) {
'Group',
function($scope, gettext, User, Group) {
// import from textarea
$scope.importByLine = function () {
$scope.users = $scope.userlist[0].split("\n");
$scope.usernames = $scope.userlist[0].split("\n");
$scope.importcounter = 0;
$scope.users.forEach(function(name) {
$scope.usernames.forEach(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).
// (e.g.: "Max Martin Mustermann" -> last_name = "Mustermann")
@ -601,42 +609,106 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
});
};
// import from csv file
// *** csv import ***
// set initial data for csv import
$scope.users = []
$scope.separator = ',';
$scope.encoding = 'UTF-8';
$scope.encodingOptions = ['UTF-8', 'ISO-8859-1'];
$scope.csv = {
content: null,
header: true,
separator: ',',
headerVisible: false,
separator: $scope.separator,
separatorVisible: false,
encoding: $scope.encoding,
encodingVisible: false,
result: null
};
$scope.importByCSV = function (result) {
var obj = JSON.parse(JSON.stringify(result));
$scope.csvimporting = true;
$scope.csvlines = Object.keys(obj).length;
$scope.csvimportcounter = 0;
for (var i = 0; i < obj.length; i++) {
var user = {};
user.title = obj[i].titel;
user.first_name = obj[i].first_name;
user.last_name = obj[i].last_name;
user.structure_level = obj[i].structure_level;
user.groups = [];
if (obj[i].groups !== '') {
var groups = obj[i].groups.replace('"','').split(",");
groups.forEach(function(group) {
user.groups.push(group);
});
// set csv file encoding
$scope.setEncoding = function () {
$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.users = [];
var quotionRe = /^"(.*)"$/;
angular.forEach($scope.csv.result, function (user) {
// title
if (user.title) {
user.title = user.title.replace(quotionRe, '$1');
}
user.comment = obj[i].comment;
User.create(user).then(
function(success) {
$scope.csvimportcounter++;
// first name
if (user.first_name) {
user.first_name = user.first_name.replace(quotionRe, '$1');
}
// last name
if (user.last_name) {
user.last_name = user.last_name.replace(quotionRe, '$1');
}
if (!user.first_name && !user.last_name) {
user.importerror = true;
user.name_error = gettext('Error: First or last name is required.');
}
// structure level
if (user.structure_level) {
user.structure_level = user.structure_level.replace(quotionRe, '$1');
}
// groups
if (user.groups) {
var csvGroups = user.groups.replace(quotionRe, '$1').split(",");
user.groups = [];
user.groupnames = [];
if (csvGroups != '') {
// All group objects are already loaded via the resolve statement from ui-router.
var allGroups = Group.getAll();
csvGroups.forEach(function(csvGroup) {
allGroups.forEach(function (allGroup) {
if (csvGroup == allGroup.id) {
user.groups.push(allGroup.id);
user.groupnames.push(allGroup.name);
}
});
});
}
);
}
} else {
user.groups = [];
}
// comment
if (user.comment) {
user.comment = user.comment.replace(quotionRe, '$1');
}
// is active
if (user.is_active) {
user.is_active = user.is_active.replace(quotionRe, '$1');
if (user.is_active == '1') {
user.is_active = true;
} else {
user.is_active = false;
}
}
$scope.users.push(user);
});
});
// import from csv file
$scope.import = function () {
$scope.csvImporting = true;
angular.forEach($scope.users, function (user) {
if (!user.importerror) {
User.create(user).then(
function(success) {
user.imported = true;
}
);
}
});
$scope.csvimported = true;
};
$scope.clear = function () {
$scope.csv.result = null;
};

View File

@ -11,7 +11,7 @@
</div>
<div class="details">
<h3 translate>Import by copy/paste</h3>
<h2 translate>Import by copy/paste</h2>
<p translate>Copy and paste your participant names in this textbox.
Keep each person in a single line.</p>
@ -23,81 +23,148 @@
<div class="clearfix">
<button ng-click="importByLine()" class="btn btn-primary pull-left" translate>Import</button>
<div class="col-xs-5" ng-if="users">
<progressbar animate="false" type="success" max="users.length" value="importcounter">
<i>{{ importcounter }} / {{ users.length }} {{ "imported" | translate }}</i>
<div class="col-xs-5" ng-if="usernames">
<progressbar animate="false" type="success" max="usernames.length" value="importcounter">
<i>{{ importcounter }} / {{ usernames.length }} {{ "imported" | translate }}</i>
</progressbar>
</div>
</div>
<div class="spacer">
<a ng-if="importcounter > 0 && importcounter == usernames.length" ui-sref="users.user.list"
class="btn btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to users overview</translate>
</a>
</div>
<hr>
<h3 translate>Import by CSV file</h3>
<p translate>Select a CSV file to import users!
<h2 translate>Import by CSV file</h2>
<p translate>Please note:</p>
<div class="block row">
<div class="title">
<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"
encoding-visible="csv.encodingVisible"></ng-csv-import>
</div>
</div>
<h4 translate>Please note:</h4>
<ul>
<li><translate>Required comma separated values</translate>:<br>
<code translate>'title, first_name, last_name, structure level, groups, comment, is active'</code>
<li><translate>Required comma or semicolon separated values with these column header names in the first row</translate>:<br>
<code>'title, first_name, last_name, structure level, groups, comment, is active'</code>
<li><translate>Default groups</translate>:
<translate>Delegate</translate> <code>3</code>,
<translate>Staff</translate> <code>4</code>
<li translate>At least first name or last name have to be filled in. All
other fields are optional and may be empty.
<li translate>The header in first line is required.
<li translate>Required CSV file encoding is UTF-8.
<li translate>Only double quotes are accepted as text delimiter (no single quotes).
<li><a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import" translate>
Use the CSV example file from OpenSlides Wiki.
</a>
</ul>
<ng-csv-import
content="csv.content"
class="import"
header="csv.header"
separator="csv.separator"
result="csv.result"></ng-csv-import>
<div ng-if="csv.result" class="col-sm-10 well">
<div ng-if="csv.result">
<h3 translate>Preview</h3>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th>
<th>#
<th translate>Title
<th translate>First name
<th translate>Last name
<th translate>Structure level
<th translate>Groups
<th translate>Comment</th>
<tbody ng-repeat="user in csv.result">
<th translate>Comment
<th translate>Is active</th>
<tbody ng-repeat="user in users">
<tr>
<td>{{ $index+1 }}
<td>{{ user.title }}
<td>{{ user.first_name }}
<td>{{ user.last_name }}
<td>{{ user.structure_level }}
<td>{{ user.groups }}
<td>{{ user.comment }}
<td class="minimum"
ng-class="{ 'text-danger': user.importerror, 'text-success': user.imported }">
<span ng-if="user.importerror">
<i class="fa fa-exclamation-triangle fa-lg"></i>
</span>
<span ng-if="!user.importerror && !user.imported">
<i class="fa fa-check-circle-o fa-lg"></i>
</span>
<span ng-if="user.imported">
<i class="fa fa-check-circle fa-lg"></i>
</span>
<td>
{{ $index + 1 }}
<td>
{{ user.title }}
<td ng-class="{ 'text-danger': user.name_error }">
<span ng-if="!user.first_name && user.name_error" title="{{ user.name_error | translate }}">
<i class="fa fa-exclamation-triangle"></i>
</span>
{{ user.first_name }}
<td ng-class="{ 'text-danger': user.name_error }">
<span ng-if="!user.last_name && user.name_error" title="{{ user.name_error | translate }}">
<i class="fa fa-exclamation-triangle"></i>
</span>
{{ user.last_name }}
<td>
{{ user.structure_level }}
<td>
<div ng-repeat="groupname in user.groupnames">
<translate>{{ groupname }}</translate>
</div>
<td>
{{ user.comment }}
<td>
<i ng-if="user.is_active" class="fa fa-check-square-o"></i>
</table>
<div class="clearfix">
<button ng-if="!csvimporting" ng-click="importByCSV(csv.result)" class="btn btn-primary pull-left" translate>Import</button>
<div ng-if="csvimporting">
<progressbar animate="false" type="success" max="csvlines" value="csvimportcounter">
<i>{{ csvimportcounter }} / {{ csvlines }} {{ "imported" | translate }}</i>
</progressbar>
<div class="text-danger">
<div ng-repeat="user in usersFailed = (users | filter:{importerror:true})"></div>
<i class="fa fa-exclamation-triangle"></i>
{{ usersFailed.length }}
<translate>participants will be not imported.</translate>
</div>
<div>
<div ng-repeat="user in usersPassed = (users | filter:{importerror:false})"></div>
<i class="fa fa-check-circle-o fa-lg"></i>
{{ users.length - usersFailed.length }}
<translate>participants will be imported.</translate>
</div>
<div ng-repeat="user in usersImported = (users | filter:{imported:true})"></div>
<div ng-if="usersImported.length > 0" class="text-success">
<hr class="smallhr">
<i class="fa fa-check-circle fa-lg"></i>
{{ usersImported.length }}
<translate>participants were successfully imported.</translate>
</div>
<div class="spacer">
<button ng-click="clear()" class="btn btn-default" translate>
Clear preview
</button>
<button ng-if="!csvImporting" ng-click="import()" class="btn btn-primary" translate>
Import {{ users.length - usersFailed.length }} participants
</button>
</div>
<div class="spacer">
<a ng-if="csvimported" ui-sref="users.user.list" class="btn btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to users overview</translate>
</a>
</div>
<a ng-if="csvimported" ui-sref="users.user.list" class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to users overview</translate>
</a>
</div>
<p>
<div class="form-group">
<button ng-if="!csvimporting" ng-click="clear()" class="btn btn-default" translate>
Clear
</button>
</div>
</div>
</div>