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', [ .controller('AgendaImportCtrl', [
'$scope', '$scope',
'gettext',
'Agenda', 'Agenda',
'Customslide', 'Customslide',
function($scope, Agenda, Customslide) { function($scope, gettext, Agenda, Customslide) {
// import from textarea // import from textarea
$scope.importByLine = function () { $scope.importByLine = function () {
$scope.items = $scope.itemlist[0].split("\n"); $scope.titleItems = $scope.itemlist[0].split("\n");
$scope.importcounter = 0; $scope.importcounter = 0;
$scope.items.forEach(function(title) { $scope.titleItems.forEach(function(title) {
var item = {title: title}; var item = {title: title};
// TODO: create all items in bulk mode // TODO: create all items in bulk mode
Customslide.create(item).then( 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 = { $scope.csv = {
content: null, content: null,
header: true, header: true,
separator: ',', headerVisible: false,
separator: $scope.separator,
separatorVisible: false,
encoding: $scope.encoding,
encodingVisible: false,
result: null result: null
}; };
$scope.importByCSV = function (result) { // set csv file encoding
var obj = JSON.parse(JSON.stringify(result)); $scope.setEncoding = function () {
$scope.csvimporting = true; $scope.csv.encoding = $scope.encoding;
$scope.csvlines = Object.keys(obj).length; };
$scope.csvimportcounter = 0; // set csv file encoding
for (var i = 0; i < obj.length; i++) { $scope.setSeparator = function () {
var item = {}; $scope.csv.separator = $scope.separator;
item.title = obj[i].title; };
item.text = obj[i].text; // detect if csv file is loaded
// TODO: save also 'duration' in related agenda item $scope.$watch('csv.result', function () {
Customslide.create(item).then( $scope.items = [];
function(success) { var quotionRe = /^"(.*)"$/;
$scope.csvimportcounter++; 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.csvimported = true;
}; };
$scope.clear = function () { $scope.clear = function () {

View File

@ -11,7 +11,7 @@
</div> </div>
<div class="details"> <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. <p translate>Copy and paste your agenda item titles in this textbox.
Keep each item in a single line.</p> Keep each item in a single line.</p>
@ -23,71 +23,121 @@ Keep each item in a single line.</p>
<div class="clearfix"> <div class="clearfix">
<button ng-click="importByLine()" class="btn btn-primary pull-left" translate>Import</button> <button ng-click="importByLine()" class="btn btn-primary pull-left" translate>Import</button>
<div class="col-xs-5" ng-if="items"> <div class="col-xs-5" ng-if="titleItems">
<progressbar animate="false" type="success" max="items.length" value="importcounter"> <progressbar animate="false" type="success" max="titleItems.length" value="importcounter">
<i>{{ importcounter }} / {{ items.length }} {{ "imported" | translate }}</i> <i>{{ importcounter }} / {{ titleItems.length }} {{ "imported" | translate }}</i>
</progressbar> </progressbar>
</div> </div>
</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> <hr>
<h3 translate>Import by CSV file</h3> <h2 translate>Import by CSV file</h2>
<p translate>Select a CSV file to import agenda items! <div class="block row">
<div class="title">
<p translate>Please note:</p> <h3 translate>Select a CSV file
<ul><!--TODO: utf-8 encoding still required with angular-csv? --> </div>
<li><translate>Required comma separated values</translate>:<br> <div class="block right import">
<code translate>'title, text'</code> <label class="label" for="inputSeparator" translate>Separator</label>
<li translate>Text and duration are optional and may be empty. <input type="text" ng-model="separator" ng-change="setSeparator()" ng-init="separator=separator" id="inputSeparator">
<li translate>The header in first line is required. <br>
<li translate>Required CSV file encoding is UTF-8. <label class="label" for="selectEncoding" translate>Encoding</label>
<li><a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import" translate> <select ng-model="encoding" ng-options="enc as enc for enc in encodingOptions"
Use the CSV example file from OpenSlides Wiki. ng-selected="setEncoding()" ng-init="encoding=encoding" id="selectEncoding"></select>
</a> <ng-csv-import
</ul> content="csv.content"
header="csv.header"
<ng-csv-import header-visible="csv.headerVisible"
content="csv.content" separator="csv.separator"
class="import" separator-visible="csv.separatorVisible"
header="csv.header" result="csv.result"
separator="csv.separator" encoding="csv.encoding"
result="csv.result"></ng-csv-import> encoding-visible="csv.encodingVisible"></ng-csv-import>
</div>
<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>
</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> <h4 translate>Please note:</h4>
<translate>Back to agenda overview</translate> <ul>
</a> <li><translate>Required comma or semicolon separated values with these column header names in the first row</translate>:<br>
</div> <code>'title, text'</code>
<p> <li translate>Text is optional and may be empty.
<div class="form-group"> <li translate>Only double quotes are accepted as text delimiter (no single quotes).
<button ng-if="!csvimporting" ng-click="clear()" class="btn btn-default" translate> <li><a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import" translate>
Clear 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> </button>
</div> </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> </div>

View File

@ -4,14 +4,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \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 #: assignments/static/templates/assignments/assignment-list.html:54
msgid "--- Select phase ---" msgid "--- Select phase ---"
msgstr "" msgstr ""
@ -20,7 +12,7 @@ msgstr ""
msgid "--- Select state ---" msgid "--- Select state ---"
msgstr "" 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-profile.html:39
#: users/static/templates/users/user-detail.html:37 #: users/static/templates/users/user-detail.html:37
msgid "About me" msgid "About me"
@ -77,7 +69,7 @@ msgstr ""
msgid "Agenda item" msgid "Agenda item"
msgstr "" msgstr ""
#: users/static/templates/users/user-import.html:45 #: users/static/templates/users/user-import.html:74
msgid "" msgid ""
"At least first name or last name have to be filled in. All\n" "At least first name or last name have to be filled in. All\n"
" other fields are optional and may be empty." " other fields are optional and may be empty."
@ -88,13 +80,11 @@ msgid "Attachment"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-detail.html:6 #: 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" msgid "Back to agenda"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:83
msgid "Back to agenda overview"
msgstr ""
#: motions/static/templates/motions/motion-import.html:136 #: motions/static/templates/motions/motion-import.html:136
msgid "Back to motions overview" msgid "Back to motions overview"
msgstr "" msgstr ""
@ -118,7 +108,8 @@ msgstr ""
msgid "Back to overview" msgid "Back to overview"
msgstr "" 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" msgid "Back to users overview"
msgstr "" msgstr ""
@ -167,12 +158,9 @@ msgstr ""
msgid "Category" msgid "Category"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:88 #: agenda/static/templates/agenda/item-import.html:129
#: users/static/templates/users/user-import.html:98
msgid "Clear"
msgstr ""
#: motions/static/templates/motions/motion-import.html:126 #: motions/static/templates/motions/motion-import.html:126
#: users/static/templates/users/user-import.html:156
msgid "Clear preview" msgid "Clear preview"
msgstr "" msgstr ""
@ -185,9 +173,9 @@ msgid "Closed"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-list.html:174 #: 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-detail.html:45
#: users/static/templates/users/user-import.html:72 #: users/static/templates/users/user-import.html:94
msgid "Comment" msgid "Comment"
msgstr "" msgstr ""
@ -233,15 +221,15 @@ msgstr ""
msgid "Default comment on the ballot paper" msgid "Default comment on the ballot paper"
msgstr "" msgstr ""
#: users/static/templates/users/user-import.html:42 #: users/static/templates/users/user-import.html:71
msgid "Default groups" msgid "Default groups"
msgstr "" msgstr ""
#: users/static/js/users/site.js:304 #: users/static/js/users/site.js:329
msgid "Default password" msgid "Default password"
msgstr "" msgstr ""
#: users/static/templates/users/user-import.html:43 #: users/static/templates/users/user-import.html:72
msgid "Delegate" msgid "Delegate"
msgstr "" msgstr ""
@ -287,7 +275,6 @@ msgstr ""
msgid "Drag and drop items to change the order of the agenda. Your modification will be saved immediately." msgid "Drag and drop items to change the order of the agenda. Your modification will be saved immediately."
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:65
#: agenda/static/templates/agenda/item-list.html:114 #: agenda/static/templates/agenda/item-list.html:114
#: agenda/static/templates/agenda/item-list.html:184 #: agenda/static/templates/agenda/item-list.html:184
msgid "Duration" msgid "Duration"
@ -379,7 +366,9 @@ msgstr ""
msgid "Elections" msgid "Elections"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:51
#: motions/static/templates/motions/motion-import.html:23 #: motions/static/templates/motions/motion-import.html:23
#: users/static/templates/users/user-import.html:52
msgid "Encoding" msgid "Encoding"
msgstr "" msgstr ""
@ -387,6 +376,10 @@ msgstr ""
msgid "English" msgid "English"
msgstr "" msgstr ""
#: users/static/js/users/site.js:652
msgid "Error: First or last name is required."
msgstr ""
#: motions/static/js/motions/site.js:618 #: motions/static/js/motions/site.js:618
msgid "Error: Identifier already exists." msgid "Error: Identifier already exists."
msgstr "" msgstr ""
@ -395,6 +388,7 @@ msgstr ""
msgid "Error: Text is required." msgid "Error: Text is required."
msgstr "" msgstr ""
#: agenda/static/js/agenda/site.js:319
#: motions/static/js/motions/site.js:628 #: motions/static/js/motions/site.js:628
msgid "Error: Title is required." msgid "Error: Title is required."
msgstr "" msgstr ""
@ -422,9 +416,9 @@ msgstr ""
msgid "Filter" msgid "Filter"
msgstr "" 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-detail-profile.html:26
#: users/static/templates/users/user-import.html:68 #: users/static/templates/users/user-import.html:90
msgid "First name" msgid "First name"
msgstr "" msgstr ""
@ -440,10 +434,10 @@ msgstr ""
msgid "Group" msgid "Group"
msgstr "" 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/group-list.html:13
#: users/static/templates/users/user-detail.html:33 #: 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:10
#: users/static/templates/users/user-list.html:109 #: users/static/templates/users/user-list.html:109
msgid "Groups" msgid "Groups"
@ -482,11 +476,9 @@ msgid "Identifier, reason, submitter and category are optional and may be empty.
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:25 #: agenda/static/templates/agenda/item-import.html:25
#: agenda/static/templates/agenda/item-import.html:75
#: agenda/static/templates/agenda/item-list.html:14 #: agenda/static/templates/agenda/item-list.html:14
#: motions/static/templates/motions/motion-list.html:18 #: motions/static/templates/motions/motion-list.html:18
#: users/static/templates/users/user-import.html:25 #: users/static/templates/users/user-import.html:25
#: users/static/templates/users/user-import.html:85
#: users/static/templates/users/user-list.html:14 #: users/static/templates/users/user-list.html:14
msgid "Import" msgid "Import"
msgstr "" msgstr ""
@ -495,8 +487,8 @@ msgstr ""
msgid "Import agenda items" msgid "Import agenda items"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:35 #: agenda/static/templates/agenda/item-import.html:42
#: users/static/templates/users/user-import.html:35 #: users/static/templates/users/user-import.html:42
msgid "Import by CSV file" msgid "Import by CSV file"
msgstr "" msgstr ""
@ -513,15 +505,23 @@ msgstr ""
msgid "Import participants" msgid "Import participants"
msgstr "" 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 #: motions/static/templates/motions/motion-import.html:129
msgid "Import {{ motions.length - motionsFailed.length }} motions" msgid "Import {{ motions.length - motionsFailed.length }} motions"
msgstr "" 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!" msgid "Important: Please change your password!"
msgstr "" msgstr ""
#: users/static/js/users/site.js:720 #: users/static/js/users/site.js:807
msgid "Installation was successfully." msgid "Installation was successfully."
msgstr "" msgstr ""
@ -531,11 +531,12 @@ msgstr ""
msgid "Invalid votes" msgid "Invalid votes"
msgstr "" 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" msgid "Is active"
msgstr "" msgstr ""
#: users/static/js/users/site.js:331 #: users/static/js/users/site.js:356
#: users/static/templates/users/user-list.html:70 #: users/static/templates/users/user-list.html:70
msgid "Is present" msgid "Is present"
msgstr "" msgstr ""
@ -544,9 +545,9 @@ msgstr ""
msgid "Item number" msgid "Item number"
msgstr "" 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-detail-profile.html:30
#: users/static/templates/users/user-import.html:69 #: users/static/templates/users/user-import.html:91
msgid "Last name" msgid "Last name"
msgstr "" msgstr ""
@ -692,7 +693,9 @@ msgstr ""
msgid "Old speakers:" msgid "Old speakers:"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:71
#: motions/static/templates/motions/motion-import.html:43 #: 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)." msgid "Only double quotes are accepted as text delimiter (no single quotes)."
msgstr "" msgstr ""
@ -743,9 +746,9 @@ msgstr ""
msgid "Phase" msgid "Phase"
msgstr "" 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 #: 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:" msgid "Please note:"
msgstr "" msgstr ""
@ -766,9 +769,9 @@ msgstr ""
msgid "Present" msgid "Present"
msgstr "" 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 #: 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" msgid "Preview"
msgstr "" msgstr ""
@ -838,20 +841,12 @@ msgstr ""
msgid "Remove message" msgid "Remove message"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:44 #: agenda/static/templates/agenda/item-import.html:68
#: users/static/templates/users/user-import.html:48
msgid "Required CSV file encoding is UTF-8."
msgstr ""
#: motions/static/templates/motions/motion-import.html:40 #: 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" msgid "Required comma or semicolon separated values with these column header names in the first row"
msgstr "" 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 #: core/static/templates/core/projector-controls.html:99
msgid "Reset countdown" msgid "Reset countdown"
msgstr "" msgstr ""
@ -899,23 +894,17 @@ msgstr ""
msgid "Select" msgid "Select"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:45
#: motions/static/templates/motions/motion-import.html:17 #: motions/static/templates/motions/motion-import.html:17
#: users/static/templates/users/user-import.html:46
msgid "Select a CSV file" msgid "Select a CSV file"
msgstr "" 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 #: motions/static/js/motions/site.js:214
msgid "Select or search a category..." msgid "Select or search a category..."
msgstr "" msgstr ""
#: users/static/js/users/site.js:297 #: users/static/js/users/site.js:322
msgid "Select or search a group..." msgid "Select or search a group..."
msgstr "" msgstr ""
@ -947,7 +936,9 @@ msgstr ""
msgid "Select or search an attachment..." msgid "Select or search an attachment..."
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:48
#: motions/static/templates/motions/motion-import.html:20 #: motions/static/templates/motions/motion-import.html:20
#: users/static/templates/users/user-import.html:49
msgid "Separator" msgid "Separator"
msgstr "" msgstr ""
@ -980,7 +971,7 @@ msgstr ""
msgid "Special values" msgid "Special values"
msgstr "" msgstr ""
#: users/static/templates/users/user-import.html:44 #: users/static/templates/users/user-import.html:73
msgid "Staff" msgid "Staff"
msgstr "" msgstr ""
@ -1005,10 +996,10 @@ msgstr ""
msgid "Stop current speaker" msgid "Stop current speaker"
msgstr "" 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-profile.html:35
#: users/static/templates/users/user-detail.html:31 #: 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 #: users/static/templates/users/user-list.html:104
msgid "Structure level" msgid "Structure level"
msgstr "" msgstr ""
@ -1052,7 +1043,7 @@ msgstr ""
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:64 #: agenda/static/templates/agenda/item-import.html:85
#: core/static/js/core/site.js:472 #: core/static/js/core/site.js:472
#: motions/static/js/motions/site.js:169 #: motions/static/js/motions/site.js:169
#: motions/static/templates/motions/motion-detail.html:232 #: motions/static/templates/motions/motion-detail.html:232
@ -1060,17 +1051,12 @@ msgstr ""
msgid "Text" msgid "Text"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:42 #: agenda/static/templates/agenda/item-import.html:70
msgid "Text and duration are optional and may be empty." msgid "Text is 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."
msgstr "" msgstr ""
#. academic degree #. 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 #: agenda/static/templates/agenda/item-list.html:170
#: assignments/static/js/assignments/site.js:77 #: assignments/static/js/assignments/site.js:77
#: assignments/static/templates/assignments/assignment-list.html:140 #: assignments/static/templates/assignments/assignment-list.html:140
@ -1081,9 +1067,9 @@ msgstr ""
#: motions/static/js/motions/site.js:161 #: motions/static/js/motions/site.js:161
#: motions/static/templates/motions/motion-import.html:57 #: motions/static/templates/motions/motion-import.html:57
#: motions/static/templates/motions/motion-list.html:95 #: 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-detail-profile.html:22
#: users/static/templates/users/user-import.html:67 #: users/static/templates/users/user-import.html:89
msgid "Title" msgid "Title"
msgstr "" msgstr ""
@ -1109,13 +1095,13 @@ msgstr ""
msgid "Uploaded by" msgid "Uploaded by"
msgstr "" 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." msgid "Use <strong>admin</strong> and <strong>admin</strong> for first login."
msgstr "" 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 #: 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." msgid "Use the CSV example file from OpenSlides Wiki."
msgstr "" msgstr ""
@ -1125,7 +1111,7 @@ msgstr ""
msgid "Username" msgid "Username"
msgstr "" msgstr ""
#: users/static/js/users/site.js:742 #: users/static/js/users/site.js:829
msgid "Username or password was not correct." msgid "Username or password was not correct."
msgstr "" msgstr ""
@ -1171,6 +1157,10 @@ msgstr ""
msgid "Yes" msgid "Yes"
msgstr "" 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 #: assignments/static/templates/assignments/assignment-list.html:72
msgid "elections" msgid "elections"
msgstr "" msgstr ""
@ -1181,9 +1171,7 @@ msgid "h"
msgstr "" msgstr ""
#: agenda/static/templates/agenda/item-import.html:28 #: 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:28
#: users/static/templates/users/user-import.html:88
msgid "imported" msgid "imported"
msgstr "" msgstr ""
@ -1191,6 +1179,14 @@ msgstr ""
msgid "items" msgid "items"
msgstr "" 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 #: motions/static/templates/motions/motion-detail.html:117
msgid "majority" msgid "majority"
msgstr "" msgstr ""
@ -1215,6 +1211,18 @@ msgstr ""
msgid "participants" msgid "participants"
msgstr "" 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 #: agenda/static/templates/agenda/item-list.html:99
#: assignments/static/templates/assignments/assignment-list.html:73 #: assignments/static/templates/assignments/assignment-list.html:73
#: motions/static/templates/motions/motion-list.html:79 #: motions/static/templates/motions/motion-list.html:79
@ -1225,3 +1233,7 @@ msgstr ""
#: motions/static/templates/motions/motion-detail.html:118 #: motions/static/templates/motions/motion-detail.html:118
msgid "undocumented" msgid "undocumented"
msgstr "" 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', { .state('users.user.import', {
url: '/import', url: '/import',
controller: 'UserImportCtrl', controller: 'UserImportCtrl',
resolve: {
groups: function(Group) {
return Group.findAll();
}
}
}) })
// groups // groups
.state('users.group', { .state('users.group', {
@ -375,6 +380,7 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
function($scope, $state, ngDialog, User, Group) { function($scope, $state, ngDialog, User, Group) {
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
Group.bindAll({}, $scope, 'groups'); Group.bindAll({}, $scope, 'groups');
$scope.alert = {};
// setup table sorting // setup table sorting
$scope.sortColumn = 'first_name'; //TODO: sort by first OR last name $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 // save changed user
$scope.save = function (user) { $scope.save = function (user) {
Assignment.save(user).then( User.save(user).then(
function(success) { function(success) {
//user.quickEdit = false; //user.quickEdit = false;
$scope.alert.show = false; $scope.alert.show = false;
@ -575,13 +581,15 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
.controller('UserImportCtrl', [ .controller('UserImportCtrl', [
'$scope', '$scope',
'gettext',
'User', 'User',
function($scope, User) { 'Group',
function($scope, gettext, User, Group) {
// import from textarea // import from textarea
$scope.importByLine = function () { $scope.importByLine = function () {
$scope.users = $scope.userlist[0].split("\n"); $scope.usernames = $scope.userlist[0].split("\n");
$scope.importcounter = 0; $scope.importcounter = 0;
$scope.users.forEach(function(name) { $scope.usernames.forEach(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).
// (e.g.: "Max Martin Mustermann" -> last_name = "Mustermann") // (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 = { $scope.csv = {
content: null, content: null,
header: true, header: true,
separator: ',', headerVisible: false,
separator: $scope.separator,
separatorVisible: false,
encoding: $scope.encoding,
encodingVisible: false,
result: null result: null
}; };
// set csv file encoding
$scope.importByCSV = function (result) { $scope.setEncoding = function () {
var obj = JSON.parse(JSON.stringify(result)); $scope.csv.encoding = $scope.encoding;
$scope.csvimporting = true; };
$scope.csvlines = Object.keys(obj).length; // set csv file encoding
$scope.csvimportcounter = 0; $scope.setSeparator = function () {
for (var i = 0; i < obj.length; i++) { $scope.csv.separator = $scope.separator;
var user = {}; };
user.title = obj[i].titel; // detect if csv file is loaded
user.first_name = obj[i].first_name; $scope.$watch('csv.result', function () {
user.last_name = obj[i].last_name; $scope.users = [];
user.structure_level = obj[i].structure_level; var quotionRe = /^"(.*)"$/;
user.groups = []; angular.forEach($scope.csv.result, function (user) {
if (obj[i].groups !== '') { // title
var groups = obj[i].groups.replace('"','').split(","); if (user.title) {
groups.forEach(function(group) { user.title = user.title.replace(quotionRe, '$1');
user.groups.push(group);
});
} }
user.comment = obj[i].comment; // first name
User.create(user).then( if (user.first_name) {
function(success) { user.first_name = user.first_name.replace(quotionRe, '$1');
$scope.csvimportcounter++; }
// 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.csvimported = true;
}; };
$scope.clear = function () { $scope.clear = function () {
$scope.csv.result = null; $scope.csv.result = null;
}; };

View File

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