diff --git a/.gitignore b/.gitignore index 686f558c4..780e93394 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.mise.toml +*.tar.gz .precomp/ tmp/ t/test-data/.secret @@ -15,5 +17,16 @@ aclocal.m4 etc/*.cfg etc/*.yaml etc/*.secret +Inline/ public/ conftools/ +test/rest/ +thirdparty/build.log +thirdparty/cache +thirdparty/carton/ +thirdparty/latest-build +thirdparty/sources +thirdparty/touch +thirdparty/lib/ +thirdparty/work/ +thirdparty/bin/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 382d3b360..f14683e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +- 6.4.1, 2024-11-22, fritz.zaucker@oetiker.ch + - New Self Service for account creation and password reset + - 6.4.0, 2023-06-22, fritz.zaucker@oetiker.ch - Allow absolute path for --technical-file diff --git a/Makefile.am b/Makefile.am index 32c4ba575..c6fda5132 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,33 +1,19 @@ -# Copyright (C) 2020 Fritz Zaucker +# Copyright (C) 2023 Fritz Zaucker -AUTOMAKE_OPTIONS = foreign +AUTOMAKE_OPTIONS = foreign -SUBDIRS = etc frontend +# SUBDIRS = thirdparty etc frontend +SUBDIRS = thirdparty frontend PUB := $(shell find -L public -type d \( -name ".??*" -o -name transpiled \) -prune -o -not -name "*db.json" -a -not -name "*.map" -a -not -name "*~" -a -not -name transpiled -a -not -name "*.tmp" -a -type f -print ) -SHARE := $(shell test -d share && find -L share -type d -name ".??*" -prune -o -not -name ".*" -a -not -name "*~" -a -not -name "*.tmp" -a -not -name '*.sql' -a -not -wholename '*/Docu/*' -a -not -wholename '*/Docu' -a -not -wholename '*/_Attic' -a -not -wholename '*/_Attic/*' -a -type f -print) -PERLTESTS := $(shell find -L t -name "*.t") -PM := $(shell find -L lib -name "*.pm") +TEMPL := $(shell test -d templates && find -L templates -type f -name "*.ep") +SHARE := $(shell test -d share && find -L share -type d -name ".??*" -prune -o -not -name ".*" -a -not -name "*~" -a -not -name "*.tmp" -a -not -path "share/Models/Experimental/*" -a -type f -print) -#EXTRA_DIST = META6.json COPYRIGHT LICENSE CHANGES AUTHORS bootstrap $(PUB) $(POD) $(TEMPL) $(SHARE) +PERLTESTS := $(shell find t -name "*.t") +PM := $(shell find lib -name "*.pm") -EXTRA_DIST = META6.json COPYRIGHT bin lib/Agrammon bootstrap $(PUB) $(SHARE) # $(PERLTESTS) +# EXTRA_DIST = VERSION cpanfile COPYRIGHT LICENSE CHANGELOG AUTHORS bootstrap $(PUB) $(wildcard t/*.t) $(POD) $(TEMPL) $(PERLTESTS) $(SHARE) Dockerfile -YEAR := $(shell date +%Y) -DATE := $(shell date +%Y-%m-%d) +EXTRA_DIST = META6.json README.md VERSION cpanfile COPYRIGHT CHANGELOG.md bootstrap $(PUB) $(wildcard t/*.t) $(POD) $(TEMPL) $(PERLTESTS) $(SHARE) bin lib -datadir = $(prefix) -nobase_data_DATA = $(PUB) $(TEMPL) $(SHARE) - -README.md COPYRIGHT: - $(PERL) -i -p -e 's/(#VERSION#|\d+\.\d+\.\d+[^.\s]*)/$(PACKAGE_VERSION)/g;s/(#YEAR#|20[1-9]\d)/$(YEAR)/g;s/(#DATE#|20[1-9]\d-[01]\d-[0-3]\d)/$(DATE)/g;' $@ - - -LANGUAGES := $(shell $(PERL) -e 'use JSON::PP qw(decode_json); print join(" ", map {"share/".$$_.".po"} @{decode_json(join("",<>))->{locales}})' frontend/compile.json) - -test: - prove -j8 -e 'raku -Ilib' t - -unit-test: - AGRAMMON_UNIT_TEST=1 prove -j8 -e 'raku -Ilib' t diff --git a/README.md b/README.md index 3cba24538..f1872e3d2 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Load a database dump (auto creation not yet implemented) ## Installation Web App +apt install libnsl-dev + Install npm and jq from your distro and then run mkdir -p public # first time only diff --git a/VERSION b/VERSION index 798e38995..4c77920fd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.3.0 +6.4.1 diff --git a/bin/Makefile.am b/bin/Makefile.am new file mode 100644 index 000000000..53e69993d --- /dev/null +++ b/bin/Makefile.am @@ -0,0 +1,7 @@ +dist_bin_SCRIPTS = $(MJ_SCRIPT).raku + +# make sure we have all Raku dependencies +# pre-compile +install-exec-hook: + zef --/test --deps-only install .. + PERL5LIB=$(prefix)/Inline/perl5 raku -I$(libdir) -c $(bindir)/$(MJ_SCRIPT).raku diff --git a/bin/agrammon.raku b/bin/agrammon.raku index 62b146c53..2cdb191b0 100755 --- a/bin/agrammon.raku +++ b/bin/agrammon.raku @@ -1,3 +1,7 @@ -#!/usr/bin/env perl6 - +#!/usr/bin/env raku +# unbuffered output +$*OUT.out-buffer = False; +$*ERR.out-buffer = False; +use lib $*PROGRAM.resolve.parent(2) ~ '/lib'; +use lib:from $*PROGRAM.resolve.parent(2) ~ '/Inline/perl5'; use Agrammon::UI::CommandLine; diff --git a/configure.ac b/configure.ac index 36df524a3..2b2716ef9 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@ # Copyright (C) 2020 Oetiker+Partner AG -AC_INIT([agrammon],m4_esyscmd([jq '.version' < META6.json | tr -d '""' ]),[fritz.zaucker@oetiker.ch]) -AC_PREREQ([2.59]) +AC_INIT([agrammon], m4_esyscmd(jq '.version' < META6.json | tr -d '""' | tr -d '\n'),[fritz.zaucker@oetiker.ch]) +AC_PREREQ([2.71]) AC_CONFIG_AUX_DIR(conftools) MJ_CLASS=Agrammon @@ -76,6 +76,9 @@ if test -x "$NODE"; then v13*) AC_MSG_RESULT(ok v13) ;; + v22*) + AC_MSG_RESULT(ok v22) + ;; *) AC_MSG_RESULT(no); NODE=old-node @@ -129,8 +132,7 @@ else fi AC_ARG_ENABLE(pkgonly, - AC_HELP_STRING([--enable-pkgonly], - [Skip all checking])) + AS_HELP_STRING([--enable-pkgonly],[Skip all checking])) AC_SUBST(enable_pkgonly) actual_prefix=$prefix @@ -154,6 +156,7 @@ AC_CONFIG_FILES([ Makefile etc/Makefile frontend/Makefile + thirdparty/Makefile ]) AC_SUBST(VERSION) diff --git a/cpanfile b/cpanfile new file mode 100644 index 000000000..ce89cddda --- /dev/null +++ b/cpanfile @@ -0,0 +1 @@ +requires 'Excel::Writer::XLSX'; diff --git a/dev/META6.json b/dev/META6.json index 3742bdaef..bef7afa06 100644 --- a/dev/META6.json +++ b/dev/META6.json @@ -1,7 +1,7 @@ { "name": "Agrammon", "description": "Simulation model for calculating ammonia and Nx/NOx emissions from agriculture.", - "version": "6.4.0", + "version": "6.4.1", "perl": "6.*", "authors": [ "Fritz Zaucker ", @@ -11,16 +11,20 @@ "depends": [ "Cro::Core", "Cro::HTTP", + "Cro::HTTP::Log::File", "Cro::HTTP::Session::Pg", "Cro::OpenAPI::RoutesFromDefinition:ver<1.0.4+>", "Cro::WebApp::Template", "Cro::APIToken", "Cro::APIToken::Store::Pg", + "Cro::HTTP::Log::File", "Crypt::Random", + "Crypt::Random::Extra", "Data::Dump::Tree", "DB::Pg", "Digest::SHA1::Native", "Email::MIME", + "Inline::Perl5", "IO::Path::ChildSecure", "IO::String", "LibXML:ver<0.7.9+>", @@ -29,6 +33,7 @@ "OO::Monitors", "Shell::Command", "Spreadsheet::XLSX:ver<0.2.4+>", + "Temp::Path", "Text::CSV", "YAMLish", "Cro::APIToken", diff --git a/frontend/Makefile.am b/frontend/Makefile.am index 67f53ed64..a42b2f305 100644 --- a/frontend/Makefile.am +++ b/frontend/Makefile.am @@ -30,6 +30,7 @@ clean: node_modules $(NPX) qx compile --target=build --feedback=false --erase --update-po-files mkdir -p ../public/$(QX_CLASS) cp -p compiled/build/$(QX_CLASS)/*.js ../public/$(QX_CLASS) + cp -p compiled/build/$(QX_CLASS)/*.js.map ../public/$(QX_CLASS) cp -p compiled/build/index.html ../public cp -pa compiled/build/resource ../public touch $@ diff --git a/frontend/source-sha b/frontend/source-sha deleted file mode 100644 index d474bdc01..000000000 --- a/frontend/source-sha +++ /dev/null @@ -1 +0,0 @@ -9d3fb50351eac9174683a44344625a36dcedf9a0 \ No newline at end of file diff --git a/frontend/source/class/agrammon/Application.js b/frontend/source/class/agrammon/Application.js index 00dede8d3..bcd3a96d6 100644 --- a/frontend/source/class/agrammon/Application.js +++ b/frontend/source/class/agrammon/Application.js @@ -16,10 +16,10 @@ qx.Class.define('agrammon.Application', { main: function() { this.base(arguments); - var rv = -1; // Return value assumes failure. + let rv = -1; // Return value assumes failure. if (navigator.appName == 'Microsoft Internet Explorer') { - var ua = navigator.userAgent; - var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + let ua = navigator.userAgent; + let re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); if (re.exec(ua) != null) { rv = parseFloat( RegExp.$1 ); } @@ -37,24 +37,24 @@ qx.Class.define('agrammon.Application', { qx.log.appender.Console; } - var param, params = this.__getParams(); - for (var i=0; i 0) { urlParams = urlParams.substr(1, urlParams.length); if (params != null) { @@ -228,14 +223,9 @@ qx.Class.define('agrammon.Application', { }, __login: function(msg) { - var userData = msg.getData(); + let userData = msg.getData(); this.__retry = userData.retry; this.debug('__login(' + userData.username + ')'); - if (this.__supports_html5_storage() && userData.remember) { - localStorage.setItem('agrammonUsername', userData.username); - localStorage.setItem('agrammonPassword', userData.password); - localStorage.setItem('agrammonRemember', userData.remember); - } if (userData.sudoUsername === undefined) { userData.sudoUsername = null; } @@ -243,16 +233,18 @@ qx.Class.define('agrammon.Application', { }, __logout: function() { + // console.log('Application.__logout()'); this.__rpc.callAsync( qx.lang.Function.bind(this.__logoutFunc,this), 'logout'); qx.event.message.Bus.dispatchByName('agrammon.NavBar.clearTree', null); qx.event.message.Bus.dispatchByName('agrammon.input.select'); }, __logoutFunc: function(data, exc, id) { + // console.log('Application.__logoutFunc():', data, exc, id); if (exc == null || exc == 403) { - if (data.sudoUser) { - var infoOnly = true; - var dialog = new agrammon.ui.dialog.Confirm( + if (data && data.sudoUser) { + let infoOnly = true; + let dialog = new agrammon.ui.dialog.Confirm( this.tr("End change user"), this.tr("Returning from %1 to %2", data.sudoUser, data.username), qx.lang.Function.bind(function() { @@ -270,7 +262,7 @@ qx.Class.define('agrammon.Application', { else { alert(exc); } - new agrammon.module.user.Login(this.tr("Please authenticate yourself")).open(); + this.__loginDialog.open(); } } }); diff --git a/frontend/source/class/agrammon/io/remote/Rpc.js b/frontend/source/class/agrammon/io/remote/Rpc.js index 4815eb646..be99e9e2b 100644 --- a/frontend/source/class/agrammon/io/remote/Rpc.js +++ b/frontend/source/class/agrammon/io/remote/Rpc.js @@ -51,7 +51,7 @@ qx.Class.define('agrammon.io.remote.Rpc', { * @param data {Map} the data to send. */ callAsync : function(handler, methodName, data) { - var req = new qx.io.request.Xhr(methodName, "POST"); + let req = new qx.io.request.Xhr(methodName, "POST"); if (data != null) { req.setRequestData(data); req.setRequestHeader("Content-Type", "application/json"); @@ -60,8 +60,8 @@ qx.Class.define('agrammon.io.remote.Rpc', { this.__pending = { methodName : methodName, data : data, handler : handler }; } req.addListener("statusError", function(e) { - var req = e.getTarget(); - this.handleStatusError(req, methodName); + let req = e.getTarget(); + this.handleStatusError(req, methodName, handler, data); }, this); let that = this; req.addListener("success", function(e) { @@ -84,18 +84,19 @@ qx.Class.define('agrammon.io.remote.Rpc', { req.send(); }, - handleStatusError : function(req, methodName) { - var response = req.getResponse(); - var status = req.getStatus(); - var statusText = req.getStatusText(); - console.error('Rpc.callAsync('+methodName+'): status=', status, ':', statusText, ', response=', response); + handleStatusError : function(req, methodName, handler, data) { + let response = req.getResponse(); + let status = req.getStatus(); + let statusText = req.getStatusText(); + console.log('Rpc.callAsync('+methodName+'): status=', status, ':', statusText, ', response=', response); + let username = agrammon.Info.getInstance().getUserName(); if (response && response.error) { - var params = [ + let params = [ qx.locale.Manager.tr("Error") + ' ' + status, response.error, 'error', ]; - if (!agrammon.Info.getInstance().getUserName()) { + if (!username) { params.push( { msg: 'agrammon.main.logout', data: null} ); } // no results @@ -107,11 +108,13 @@ qx.Class.define('agrammon.io.remote.Rpc', { else { let retry = true; let sudo = null; - let title; + let title = qx.locale.Manager.tr("Please authenticate yourself"); switch (status) { case 404: case 401: - title = qx.locale.Manager.tr("%1: Session expired: please login again", status); + if (username) { + title = qx.locale.Manager.tr("%1: Session expired: please login again", status); + } break; default: title = qx.locale.Manager.tr("Error %1: %2 - please login again", status, statusText); @@ -119,6 +122,7 @@ qx.Class.define('agrammon.io.remote.Rpc', { } new agrammon.module.user.Login(title, sudo, retry).open(); } + handler(data); }, /* A variant of the asyncCall method which pops up error messages @@ -132,9 +136,9 @@ qx.Class.define('agrammon.io.remote.Rpc', { * @return {var} the method call reference. */ callAsyncSmart : function(handler, methodName) { - var origHandler = handler; + let origHandler = handler; - var superHandler = function(ret, exc, id) { + let superHandler = function(ret, exc, id) { if (exc) { agrammon.ui.dialog.MsgBox.getInstance().exc(exc); } else { diff --git a/frontend/source/class/agrammon/module/Main.js b/frontend/source/class/agrammon/module/Main.js index cc9b9fd02..32791262b 100644 --- a/frontend/source/class/agrammon/module/Main.js +++ b/frontend/source/class/agrammon/module/Main.js @@ -15,12 +15,12 @@ qx.Class.define('agrammon.module.Main', this.__selectInput, this); this.set({padding:0}); - var report = new agrammon.module.output.Reports(output, reference); + let report = new agrammon.module.output.Reports(output, reference); this.add(input); this.add(report); // Thomas Kupper: not needed -// var graph = new agrammon.module.output.Graphs(output, reference); +// let graph = new agrammon.module.output.Graphs(output, reference); // this.add(graph); }, // construct diff --git a/frontend/source/class/agrammon/module/dataset/DatasetTool.js b/frontend/source/class/agrammon/module/dataset/DatasetTool.js index fc9a10409..bb5796ed9 100644 --- a/frontend/source/class/agrammon/module/dataset/DatasetTool.js +++ b/frontend/source/class/agrammon/module/dataset/DatasetTool.js @@ -113,12 +113,12 @@ qx.Class.define('agrammon.module.dataset.DatasetTool', { // collect all tags of the selected datasets into the tag hash and count them for (t=0; t 0) { activeTagsTm.addRows(that.__activeTags); @@ -923,6 +923,7 @@ qx.Class.define('agrammon.module.dataset.DatasetTool', { * @lint ignoreDeprecated(alert) */ __rename_tag_func: function(data,exc,id) { + console.log('rename_tag_func', data, exc, id); if (exc) { alert(exc); } @@ -954,8 +955,10 @@ qx.Class.define('agrammon.module.dataset.DatasetTool', { var row = data[0]['minIndex']; var tm = table.getTableModel(); var tag_old = tm.getValue(0, row, 1); + console.log('tag_old=', tag_old); var okFunction = qx.lang.Function.bind(function(self) { var tag_new = self.nameField.getValue(); + console.log('tag_new=', tag_new); if (this.__datasetCache.tagExists(tag_new)) { qx.event.message.Bus.dispatchByName('error', [ this.tr('Error'), @@ -964,11 +967,14 @@ qx.Class.define('agrammon.module.dataset.DatasetTool', { self.close(); return; } - + console.log('tag_new=', tag_new); tm.setValue(0, row, tag_new, 1); + console.log('calling renameTag on __availableTagsTable=', this.__availableTagsTable); this.__availableTagsTable.renameTag(tag_old, tag_new); + console.log('calling __datasetTable.renameTag'); this.__datasetTable.renameTag(tag_old, tag_new); + console.log('calling async rename_tag'); this.__rpc.callAsync( qx.lang.Function.bind(this.__rename_tag_func,this), 'rename_tag', @@ -976,7 +982,7 @@ qx.Class.define('agrammon.module.dataset.DatasetTool', { oldName: tag_old, newName: tag_new } - ); + ); self.close(); }, this); dialog = new agrammon.ui.dialog.Dialog( diff --git a/frontend/source/class/agrammon/module/dataset/TagTable.js b/frontend/source/class/agrammon/module/dataset/TagTable.js index 07006be77..d7eebde68 100644 --- a/frontend/source/class/agrammon/module/dataset/TagTable.js +++ b/frontend/source/class/agrammon/module/dataset/TagTable.js @@ -44,11 +44,11 @@ qx.Class.define('agrammon.module.dataset.TagTable', { } }, this); - this.__table.setMinWidth(180); + this.__table.setMinWidth(180); this.__table.setAllowGrowX(true); this.setPadding(0); this.add(this.__table, {flex: 1}); - + this.setFilter({}); }, // construct members : @@ -78,6 +78,7 @@ qx.Class.define('agrammon.module.dataset.TagTable', { }, setFilter: function(filter) { + // console.log('setFilter', filter); if (filter == {}) { this.__filterHash = {'*all*': true}; } @@ -128,15 +129,17 @@ qx.Class.define('agrammon.module.dataset.TagTable', { }, renameTag: function(tag_old, tag_new) { - + console.log('renameTag', tag_old, tag_new); var i, tm=this.__table.getTableModel(); var tag, len = tm.getRowCount(0); + console.log('renaming'); for (i=0; i= 6; + var valid = value != null && value.length >= 8; if (!valid) { - item.setInvalidMessage(that.tr("Password must have at least 6 characters.")); + item.setInvalidMessage(that.tr("Password must have at least 8 characters.")); } return valid; }; @@ -63,7 +65,7 @@ qx.Class.define('agrammon.module.user.Account', { this.user.setPadding(5); this.password1 = - new agrammon.ui.form.VarPassword(this.tr("Password (minimum 6 characters)"), + new agrammon.ui.form.VarPassword(this.tr("Password (minimum 8 characters)"), '', '', ''); pbox.add(this.password1); this.password1.setPadding(5); @@ -130,9 +132,9 @@ qx.Class.define('agrammon.module.user.Account', { if (!adminCreate && !adminReset) { - var msg = this.tr("An activation key will be sent to you by eMail after pressing the button"); + var msg = this.tr("An activation link will be sent to you by eMail after pressing the button"); this.msg1 = - new qx.ui.basic.Label(''+ msg + ' ' + this.msg0 + '.').set({rich: true}); + new qx.ui.basic.Label(''+ msg + ' ' + this.msg0 + '').set({rich: true}); this.msg1.setPaddingLeft(5); this.msg1.setPaddingRight(5); this.msg1.setPaddingTop(5); @@ -140,11 +142,11 @@ qx.Class.define('agrammon.module.user.Account', { pbox.add(this.msg1); } - var key = - new agrammon.ui.form.VarInput(this.tr("Key (sent by eMail)"), - '', '', ''); - this.key = key; - key.setPadding(5); + // var key = + // new agrammon.ui.form.VarInput(this.tr("Key (sent by eMail)"), + // '', '', ''); + // this.key = key; + // key.setPadding(5); var bbox = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); // bbox.set({height: 'auto', width:'auto', padding: 5}); @@ -234,28 +236,28 @@ qx.Class.define('agrammon.module.user.Account', { this.password1.setEnabled(false); this.password2.setEnabled(false); - if (passwordReset && ! adminReset) { - this.location = this.password2; - this.action = this.tr("Enter key below to re-activate your account"); - } - else if (!adminCreate) { - this.location = this.organisation; - this.action = this.tr("Enter key below to activate your account"); - } - // reverse order! - if (!adminCreate && !adminReset) { - this.pbox._addAfter(this.key, this.location); - this.msg1.setValue('' - + this.action + ''); - this.pbox._addAfter(this.msg1, this.location); - } + // if (passwordReset && ! adminReset) { + // this.location = this.password2; + // this.action = this.tr("Enter key below to re-activate your account"); + // } + // else if (!adminCreate) { + // this.location = this.organisation; + // this.action = this.tr("Enter key below to activate your account"); + // } + // // reverse order! + // if (!adminCreate && !adminReset) { + // this.pbox._addAfter(this.key, this.location); + // this.msg1.setValue('' + // + this.action + ''); + // this.pbox._addAfter(this.msg1, this.location); + // } var action; if (adminCreate) { action = 'create_account'; } else { - action = 'get_account_key'; + action = 'activate_account'; } var firstName, lastName, org; // not for self password reset diff --git a/frontend/source/class/agrammon/module/user/Login.js b/frontend/source/class/agrammon/module/user/Login.js index cf9247c16..1abcf398a 100644 --- a/frontend/source/class/agrammon/module/user/Login.js +++ b/frontend/source/class/agrammon/module/user/Login.js @@ -13,24 +13,32 @@ qx.Class.define('agrammon.module.user.Login', { construct: function (title, sudo, retry) { this.base(arguments); - this.__rpc = agrammon.io.remote.Rpc.getInstance(); - this.__baseUrl = this.__rpc.getBaseUrl(); + this.__baseUrl = agrammon.io.remote.Rpc.getInstance().getBaseUrl(); this.setLayout(new qx.ui.layout.HBox(10)); qx.core.Id.getInstance().register(this, "Login"); this.setQxObjectId("Login"); - var that = this; + // content of the form elements if they appear inside a form AND + // the form has a name (firefox comes to mind). + var el = this.getContentElement(); + var form = new qx.html.Element('form',null,{name: 'cbLoginform', autocomplete: 'on'}); + form.insertBefore(el); + el.insertInto(form); + + let that = this; this.set({ modal: true, showClose: false, showMinimize: false, showMaximize: false, - centerOnAppear : true, - caption: title + centerOnAppear : true }); + if (title !== undefined) { + this.setCaption(title); + } - var leftBox = + let leftBox = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); leftBox.setWidth(300); - var rightBox = + let rightBox = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); this.add(leftBox, {flex:1}); this.add(new qx.ui.core.Spacer(50)); @@ -40,13 +48,13 @@ qx.Class.define('agrammon.module.user.Login', { qx.locale.Manager.getInstance().addListener("changeLocale", this.__changeLanguage, this); - var user = this.__user = new agrammon.ui.form.VarInput(this.tr("Username"), '', '', '', 'Enter username'); + let user = this.__user = new agrammon.ui.form.VarInput(this.tr("Username"), '', '', '', 'Enter username'); this.addOwnedQxObject(user, "Username"); leftBox.add(user); user.setPadding(5); user.setPaddingBottom(0); - var password = this.__password = new agrammon.ui.form.VarPassword(this.tr("Password")); + let password = this.__password = new agrammon.ui.form.VarPassword(this.tr("Password")); this.addOwnedQxObject(password, "Password"); password.setPadding(5); password.setPaddingTop(0); @@ -54,38 +62,32 @@ qx.Class.define('agrammon.module.user.Login', { leftBox.add(password); } - if (this.supports_html5_storage() && !sudo) { - var remember = new qx.ui.form.CheckBox(this.tr("Remember")); - this.__remember = remember; - leftBox.add(remember); - } - leftBox.add(new qx.ui.core.Spacer(50)); - var bbox = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + let bbox = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); bbox.setPaddingLeft(5); bbox.setPaddingRight(5); leftBox.add(bbox); - var btnOK = + let btnOK = new qx.ui.form.Button("Login", "icon/16/actions/dialog-ok.png"); this.addOwnedQxObject(btnOK, "LoginButton"); - var btnNew = + let btnNew = new qx.ui.form.Button(this.tr("Create New Account"), "icon/16/actions/dialog-ok.png"); this.addOwnedQxObject(btnNew, "NewButton"); this.btnNew = btnNew; - var btnPassword = + let btnPassword = new qx.ui.form.Button(this.tr("Reset Password"), "icon/16/actions/dialog-ok.png"); this.addOwnedQxObject(btnPassword, "PasswordButton"); this.btnPassword = btnPassword; - var btnHelp = + let btnHelp = new qx.ui.form.Button(this.tr("Help"), "agrammon/help-about.png"); this.addOwnedQxObject(btnHelp, "HelpButton"); - var btnCancel = + let btnCancel = new qx.ui.form.Button(this.tr("Cancel"), "icon/16/actions/dialog-cancel.png"); this.addOwnedQxObject(btnCancel, "CancelButton"); @@ -98,9 +100,9 @@ qx.Class.define('agrammon.module.user.Login', { }, this); // FIX ME: deal with sub locales - var locale = qx.locale.Manager.getInstance().getLocale(); + let locale = qx.locale.Manager.getInstance().getLocale(); locale = locale.replace(/_.+/,''); - var help = this.__help = new agrammon.ui.dialog.DocWindow( + let help = this.__help = new agrammon.ui.dialog.DocWindow( this.tr("Help"), this.__baseUrl + 'doc/login.' + locale + '.html', this.getQxObjectId() @@ -113,35 +115,29 @@ qx.Class.define('agrammon.module.user.Login', { }, this); btnOK.addListener("execute", function(e) { - var username = this.__user.getValue(); - var password = this.__password.getValue(); - var remember = false; - if (that.supports_html5_storage() && !sudo) { - remember = that.__remember.getValue(); - } + let username = this.__user.getValue(); + let password = this.__password.getValue(); this.__password.clearValue(); qx.event.message.Bus.dispatchByName( 'agrammon.main.login', { username : username, password : password, - remember : remember, sudo : sudo, retry : retry + sudo : sudo, retry : retry } ); this.close(); }, this); btnNew.addListener("execute", function(e) { - var username = this.__user.getValue(); - var newDialog = new agrammon.module.user.Account(this.tr("Create new account"), username, 'userCreate'); + let newDialog = new agrammon.module.user.SelfService(this.tr("Create new account"), 'userCreate'); newDialog.open(); this.__password.clearValue(); this.close(); } ,this); btnPassword.addListener("execute", function(e) { - var username = this.__user.getValue(); - var newDialog = new agrammon.module.user.Account(this.tr("Reset password"), username, 'reset'); + let newDialog = new agrammon.module.user.SelfService(this.tr("Reset password"), 'reset'); newDialog.open(); this.__password.clearValue(); this.close(); @@ -160,12 +156,9 @@ qx.Class.define('agrammon.module.user.Login', { } }); - var idx = 1; + let idx = 1; user.setTabIndex(idx++); password.setTabIndex(idx++); - if (this.supports_html5_storage() && !sudo) { - remember.setTabIndex(idx++); - } btnCancel.setTabIndex(idx++); btnOK.setTabIndex(idx++); btnHelp.setTabIndex(idx++); @@ -173,23 +166,6 @@ qx.Class.define('agrammon.module.user.Login', { btnNew.setTabIndex(idx++); this.addListener('appear', function() { - var appUsername, appPassword, appRemember; - if (this.supports_html5_storage()) { - appUsername = localStorage.getItem('agrammonUsername'); - appPassword = localStorage.getItem('agrammonPassword'); - appRemember = (localStorage.getItem('agrammonRemember') == 'true'); - } - - if (remember != undefined && appRemember != null) { - remember.setValue(appRemember); - } - if (appUsername != null && !sudo) { - user.setValue(appUsername); - } - if (appPassword != null) { - password.setValue(appPassword); - } - if (!user.getValue()) { user.focus(); } @@ -201,29 +177,20 @@ qx.Class.define('agrammon.module.user.Login', { } }, this); - this.addListener('disappear', () => this.destroy(), this); + // this.addListener('disappear', () => this.destroy(), this); }, // construct members : { - __rpc : null, __baseUrl : null, __user : null, __help : null, __password : null, - supports_html5_storage: function() { - try { - return 'localStorage' in window && window['localStorage'] !== null; - } catch (e) { - return false; - } - }, - __changeLanguage: function() { // FIX ME: deal with sub locales - var locale = qx.locale.Manager.getInstance().getLocale(); + let locale = qx.locale.Manager.getInstance().getLocale(); locale = locale.replace(/_.+/,''); this.debug('Login: locale='+locale); this.__help.setSource(this.__baseUrl + 'doc/login.' + locale + '.html'); diff --git a/frontend/source/class/agrammon/module/user/SelfService.js b/frontend/source/class/agrammon/module/user/SelfService.js new file mode 100644 index 000000000..634100e97 --- /dev/null +++ b/frontend/source/class/agrammon/module/user/SelfService.js @@ -0,0 +1,263 @@ +/* ************************************************************************ + +************************************************************************ */ + +/* + * @asset(qx/icon/${qx.icontheme}/16/actions/dialog-cancel.png) + * @asset(qx/icon/${qx.icontheme}/16/actions/dialog-ok.png) + */ + +/* TODO: cleanup together with Account.js */ + +qx.Class.define('agrammon.module.user.SelfService', { + extend: qx.ui.window.Window, + + /** + * TODOC + * + * @return {var} TODOC + * @lint ignoreDeprecated(alert) + */ + construct: function (title, action) { + this.base(arguments); + let that = this; + this.set({ layout:new qx.ui.layout.VBox(10), + width: 200, modal: true, + showClose: false, showMinimize: false, showMaximize: false, + caption: title + }); + + this.__rpc = agrammon.io.remote.Rpc.getInstance(); + let rpcAction; + + let passwordReset = (action == 'reset'); + // create the form manager + let manager = new qx.ui.form.validation.Manager(); + // create a validator function + let passwordLengthValidator = function(value, item) { + let valid = value != null && value.length >= 8; + if (!valid) { + item.setInvalidMessage(that.tr("Password must have at least 8 characters.")); + } + return valid; + }; + + let pbox = new qx.ui.container.Composite(new qx.ui.layout.VBox()); + this.pbox = pbox; + this.add(pbox); + + if (passwordReset) { + this.user = new agrammon.ui.form.VarInput( + this.tr("eMail (your username)"), + '', '', '' + ); + } + else { + this.user = new agrammon.ui.form.VarInput( + this.tr("eMail (will be your username)"), + '', '', '' + ); + } + pbox.add(this.user); + this.user.setPadding(5); + + this.password1 = new agrammon.ui.form.VarPassword( + this.tr("Password (minimum 8 characters)"), + '', '', '' + ); + pbox.add(this.password1); + this.password1.setPadding(5); + + this.password2 = new agrammon.ui.form.VarPassword( + this.tr("Repeat Password"), + '', '', '' + ); + pbox.add(this.password2); + this.password2.setPadding(5); + + let msg; + + if (!passwordReset) { + let firstName = new agrammon.ui.form.VarInput( + this.tr("First name (optional)"), + '', '', '' + ); + this.firstName = firstName; + firstName.setPadding(5); + pbox.add(firstName); + + let lastName = new agrammon.ui.form.VarInput( + this.tr("Last name (optional)"), + '', '', '' + ); + this.lastName = lastName; + pbox.add(lastName); + lastName.setPadding(5); + + let organisation = new agrammon.ui.form.VarInput( + this.tr("Organisation (optional)"), + '', '', '' + ); + this.organisation = organisation; + pbox.add(organisation); + organisation.setPadding(5); + + this.msg0 = this.tr("Create Account"); + // rpcAction = 'self_create_account'; + rpcAction = 'create_account'; + msg = this.tr("An activation link will be sent to you by eMail."); + } + else { + this.setCaption(this.tr("Reset password")); + this.msg0 = this.tr("Reset Password"); + // rpcAction = 'self_reset_password'; + rpcAction = 'reset_password'; + msg = this.tr("A confirmation link will be sent to you by eMail."); + } + this.setCaption(this.msg0); + + + // add the email with a predefined email validator + manager.add(this.user.getInputField(), qx.util.Validate.email()); + // add the password fields with the notEmpty validator + manager.add(this.password1.getInputField(), passwordLengthValidator); + manager.add(this.password2.getInputField(), passwordLengthValidator); + + // add a validator to the manager itself (passwords must be equal) + manager.setValidator(function(items) { + let valid = that.password1.getValue() == that.password2.getValue(); + if (!valid) { + let message = that.tr("Passwords must be equal."); + that.password2.getInputField().setInvalidMessage(message); + that.password2.getInputField().setValid(false); + } + return valid; + }); + + // add a listener to the form manager for the validation complete + manager.addListener("complete", function() { + if (! manager.getValid()) { + alert(manager.getInvalidMessages().join("\n")); + } + }, this); + + + this.msg1 = new qx.ui.basic.Label( + ''+ msg + '' + ).set({rich: true}); + this.msg1.setPaddingLeft(5); + this.msg1.setPaddingRight(5); + this.msg1.setPaddingTop(5); + + pbox.add(this.msg1); + + let bbox = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + // bbox.set({height: 'auto', width:'auto', padding: 5}); + bbox.setPaddingLeft(5); + bbox.setPaddingRight(5); + this.add(bbox); + + let btnOK = new qx.ui.form.Button(this.msg0, "icon/16/actions/dialog-ok.png"); + this.btnOK = btnOK; + bbox.add(btnOK); + + this.addListener('keydown', function(e) { + if (e.getKeyIdentifier() == 'Enter') { + btnOK.execute(); + } + }); + + let btnCancel = new qx.ui.form.Button(this.tr("Cancel"), "icon/16/actions/dialog-cancel.png"); + bbox.add(btnCancel); + btnCancel.addListener("execute", function(e) { + this.close(); + qx.event.message.Bus.dispatchByName('agrammon.main.logout'); + }, this); + + + let accountHandler = function(data,exc,id) { + console.log('accountHandler():', data); + if (exc == null) { + if (data.key && data.username) { + agrammon.ui.dialog.MsgBox.getInstance().info( + that.tr("Account creation successful"), + that.tr("Account activation key sent to %1", data.username) + ); + } + } + else { + agrammon.ui.dialog.MsgBox.getInstance().exc(exc); + } + qx.event.message.Bus.dispatchByName('agrammon.main.logout'); + that.close(); + }; + + + let createAccount = function(e) { + this.debug('createAccount(): this='+this); + if (! manager.validate()) { + that.debug('createAccount(): Form is invalid'); + return; + } + let username = this.user.getValue(); + let password = this.password1.getValue(); + let firstName = this.firstName.getValue(); + let lastName = this.lastName.getValue(); + let org = this.organisation.getValue(); + let locale = qx.locale.Manager.getInstance().getLocale().replace(/_.+/,''); + + this.__rpc.callAsync( + accountHandler, + rpcAction, + { + email: username, + password: password, + firstname: firstName, + lastname: lastName, + org: org, + language: locale + } + ); + }; + + let resetPassword = function(e) { + if (! manager.validate()) { + that.debug('resetPassword(): Form is invalid'); + return; + } + let username = this.user.getValue(); + let password = this.password1.getValue(); + + this.__rpc.callAsync( + accountHandler, + rpcAction, + { + email: username, + password: password, + } + ); + }; + + + if (passwordReset) { + btnOK.addListener("execute", resetPassword, this); + } + else { + btnOK.addListener("execute", createAccount, this); + } + + this.center(); + return this; + }, // construct + + members : + { + __rpc: null, + user: null, + password1: null, + password2: null, + firstName: null, + lastName: null, + organisation: null + } +}); diff --git a/frontend/source/class/agrammon/ui/dialog/MsgBox.js b/frontend/source/class/agrammon/ui/dialog/MsgBox.js index fc8aa9100..278455955 100644 --- a/frontend/source/class/agrammon/ui/dialog/MsgBox.js +++ b/frontend/source/class/agrammon/ui/dialog/MsgBox.js @@ -21,7 +21,7 @@ */ /** - * A status window singelton. There is only one instance, several calls to + * A status window singleton. There is only one instance, several calls to * open will just change the windows content on the fly. * *
diff --git a/frontend/source/class/agrammon/ui/menu/FileMenu.js b/frontend/source/class/agrammon/ui/menu/FileMenu.js
index 53d10b5fa..6af4f6f88 100644
--- a/frontend/source/class/agrammon/ui/menu/FileMenu.js
+++ b/frontend/source/class/agrammon/ui/menu/FileMenu.js
@@ -176,9 +176,6 @@ qx.Class.define('agrammon.ui.menu.FileMenu', {
         }, this);
         var logoutButton =
             new qx.ui.menu.Button(this.tr("Logout"), null, logoutCommand);
-        logoutButton.addListener("execute", function(e) {
-            qx.event.message.Bus.dispatchByName('agrammon.main.logout');
-        }, this);
 
         // qxObjectIds for testing
         this.addListenerOnce('appear', () => {
diff --git a/frontend/source/class/agrammon/ui/menu/ModelMenu.js b/frontend/source/class/agrammon/ui/menu/ModelMenu.js
new file mode 100644
index 000000000..fc7c066bc
--- /dev/null
+++ b/frontend/source/class/agrammon/ui/menu/ModelMenu.js
@@ -0,0 +1,29 @@
+/* ************************************************************************
+
+************************************************************************ */
+
+qx.Class.define('agrammon.ui.menu.ModelMenu', {
+    extend: qx.ui.menu.Menu,
+
+    construct: function (modelVersions) {
+        this.base(arguments);
+console.log('ModelMenu: modelVersions=', modelVersions);
+        modelVersions.split(',').forEach(model => {
+            this.debug('model=', model);
+            let cmd = new qx.ui.command.Command();
+            cmd.addListener('execute', () => {
+                console.log('modelVersion=', model);
+                // agrammon.module.model.Model.getInstance().setModel(model);
+            });
+            this.add(new qx.ui.menu.Button(this.tr('Version %1', model), null, cmd));
+        });
+
+    }, // construct
+
+    members :
+    {
+        // __selectModel: function(model) {
+        //     console.log('__selectModel():', model);
+        // }
+    }
+});
diff --git a/frontend/source/translation/de.po b/frontend/source/translation/de.po
index 6cf53bbe3..3d6867328 100644
--- a/frontend/source/translation/de.po
+++ b/frontend/source/translation/de.po
@@ -22,7 +22,7 @@ msgstr "Benutzerwechsel beendet"
 msgid "Returning from %1 to %2"
 msgstr "Zurück von %1 zu %2"
 
-#: agrammon/Application.js
+#: agrammon/io/remote/Rpc.js
 msgid "Please authenticate yourself"
 msgstr "Bitte melden Sie sich an"
 
@@ -354,7 +354,7 @@ msgstr "Total"
 msgid "Branch configuration"
 msgstr "Branch-Konfiguration"
 
-#: agrammon/module/output/SubmitWindow.js
+#: agrammon/module/user/Account.js
 msgid "Cancel"
 msgstr "Abbrechen"
 
@@ -526,10 +526,6 @@ msgstr "Report erfolgreich abgeschickt."
 msgid "Report submission failed."
 msgstr "Report konnte nicht geschickt werden."
 
-#: agrammon/module/user/Account.js
-msgid "Password must have at least 6 characters."
-msgstr "Das Passwort muss mindestens 6 Zeichen lang sein."
-
 #: agrammon/module/user/Account.js
 msgid "eMail (your username)"
 msgstr "eMail (Ihr Benutzername)"
@@ -543,8 +539,8 @@ msgid "eMail (will be your username)"
 msgstr "eMail (wird Ihr Benutzername sein)"
 
 #: agrammon/module/user/Account.js
-msgid "Password (minimum 6 characters)"
-msgstr "Passwort (mindestens 6 Zeichen)"
+msgid "Password (minimum 8 characters)"
+msgstr "Passwort (mindestens 8 Zeichen)"
 
 #: agrammon/module/user/Account.js
 msgid "Repeat Password"
@@ -574,11 +570,7 @@ msgstr "Passwort neu setzen"
 msgid "Passwords must be equal."
 msgstr "Die Passwort-Eingaben müssen identisch sein."
 
-#: agrammon/module/user/Account.js
-msgid "An activation key will be sent to you by eMail after pressing the button"
-msgstr "Ein Aktivierungsschlüssel wird Ihnen per eMail zugesandt nach Klicken auf"
-
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Key (sent by eMail)"
 msgstr "Schlüssel (geschickt per eMail)"
 
@@ -602,7 +594,7 @@ msgstr "Passwort zurück setzen"
 msgid "Login"
 msgstr "Anmelden"
 
-#: agrammon/ui/menu/AdminMenu.js
+#: agrammon/module/user/Account.js
 msgid "Reset password"
 msgstr "Passwort zurücksetzen"
 
@@ -610,11 +602,11 @@ msgstr "Passwort zurücksetzen"
 msgid "Activate new account"
 msgstr "Konto neu aktivieren"
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Enter key below to re-activate your account"
 msgstr "Geben Sie den Schlüssel ein, um Ihr Konto zu re-aktivieren."
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Enter key below to activate your account"
 msgstr "Geben Sie den Schlüssel ein, um Ihr Konto zu aktivieren."
 
@@ -906,7 +898,7 @@ msgstr "Gültigkeit der Eingabewerte"
 msgid "Help text currently only available in German"
 msgstr "Hilfe-Text im Moment nur auf Deutsch verfügbar "
 
-#: agrammon/module/user/Login.js
+#. NO LONGER USED
 msgid "Remember"
 msgstr "Merken"
 
@@ -1000,8 +992,32 @@ msgstr ""
 
 #: agrammon/io/remote/Rest.js
 msgid "failed"
-msgstr ""
+msgstr "fehlgeschlagen"
 
 #: agrammon/io/remote/Rest.js
 msgid "Error in communication with server"
+msgstr "Fehler in der Kommunikation mit dem Server"
+
+#: agrammon/module/user/Account.js
+msgid "Password must have at least 8 characters."
+msgstr ""
+
+#: agrammon/module/user/Account.js
+msgid "An activation link will be sent to you by eMail after pressing the button"
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "An activation link will be sent to you by eMail."
+msgstr "Ein Aktivierungslink wird Ihnen per eMail zugesandt."
+
+#: agrammon/module/user/SelfService.js
+msgid "Account creation successful"
+msgstr "Konto erstellt"
+
+#: agrammon/module/user/SelfService.js
+msgid "Account activation key sent to %1"
+msgstr "Aktivierungslink geschickt an %1"
+
+#: agrammon/module/user/SelfService.js
+msgid "A confirmation link will be sent to you by eMail."
 msgstr ""
diff --git a/frontend/source/translation/en.po b/frontend/source/translation/en.po
index f66ebe6ca..a32ccea75 100644
--- a/frontend/source/translation/en.po
+++ b/frontend/source/translation/en.po
@@ -30,7 +30,7 @@ msgstr ""
 msgid "Returning from %1 to %2"
 msgstr ""
 
-#: agrammon/Application.js
+#: agrammon/io/remote/Rpc.js
 msgid "Please authenticate yourself"
 msgstr ""
 
@@ -362,7 +362,7 @@ msgstr ""
 msgid "Branch configuration"
 msgstr ""
 
-#: agrammon/module/output/SubmitWindow.js
+#: agrammon/module/user/Account.js
 msgid "Cancel"
 msgstr ""
 
@@ -530,10 +530,6 @@ msgstr ""
 msgid "Report submission failed."
 msgstr ""
 
-#: agrammon/module/user/Account.js
-msgid "Password must have at least 6 characters."
-msgstr ""
-
 #: agrammon/module/user/Account.js
 msgid "eMail (your username)"
 msgstr ""
@@ -547,7 +543,7 @@ msgid "eMail (will be your username)"
 msgstr ""
 
 #: agrammon/module/user/Account.js
-msgid "Password (minimum 6 characters)"
+msgid "Password (minimum 8 characters)"
 msgstr ""
 
 #: agrammon/module/user/Account.js
@@ -578,11 +574,11 @@ msgstr ""
 msgid "Passwords must be equal."
 msgstr ""
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "An activation key will be sent to you by eMail after pressing the button"
 msgstr ""
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Key (sent by eMail)"
 msgstr ""
 
@@ -606,7 +602,7 @@ msgstr ""
 msgid "Login"
 msgstr ""
 
-#: agrammon/ui/menu/AdminMenu.js
+#: agrammon/module/user/Account.js
 msgid "Reset password"
 msgstr ""
 
@@ -614,11 +610,11 @@ msgstr ""
 msgid "Activate new account"
 msgstr ""
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Enter key below to re-activate your account"
 msgstr ""
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Enter key below to activate your account"
 msgstr ""
 
@@ -910,7 +906,7 @@ msgstr ""
 msgid "Help text currently only available in German"
 msgstr ""
 
-#: agrammon/module/user/Login.js
+#. NO LONGER USED
 msgid "Remember"
 msgstr ""
 
@@ -1013,3 +1009,27 @@ msgstr ""
 #: agrammon/io/remote/Rest.js
 msgid "Error in communication with server"
 msgstr ""
+
+#: agrammon/module/user/Account.js
+msgid "Password must have at least 8 characters."
+msgstr ""
+
+#: agrammon/module/user/Account.js
+msgid "An activation link will be sent to you by eMail after pressing the button"
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "An activation link will be sent to you by eMail."
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "Account creation successful"
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "Account activation key sent to %1"
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "A confirmation link will be sent to you by eMail."
+msgstr ""
diff --git a/frontend/source/translation/fr.po b/frontend/source/translation/fr.po
index d2530a273..1b136f2d6 100644
--- a/frontend/source/translation/fr.po
+++ b/frontend/source/translation/fr.po
@@ -31,7 +31,7 @@ msgstr "Fin changer d'utilisateur"
 msgid "Returning from %1 to %2"
 msgstr "Retour de % 1 à% 2"
 
-#: agrammon/Application.js
+#: agrammon/io/remote/Rpc.js
 msgid "Please authenticate yourself"
 msgstr "Veuillez s'il vous plaît vous identifier"
 
@@ -363,7 +363,7 @@ msgstr ""
 msgid "Branch configuration"
 msgstr "Configuration branch"
 
-#: agrammon/module/output/SubmitWindow.js
+#: agrammon/module/user/Account.js
 msgid "Cancel"
 msgstr "Annuler"
 
@@ -543,10 +543,6 @@ msgstr "Rapport soumis avec succès."
 msgid "Report submission failed."
 msgstr "La soumission du rapport a échoué."
 
-#: agrammon/module/user/Account.js
-msgid "Password must have at least 6 characters."
-msgstr "Le mot de passe doit comporter au moins 6 caractères."
-
 #: agrammon/module/user/Account.js
 msgid "eMail (your username)"
 msgstr "e-mail (votre nom d'utilisateur)"
@@ -560,8 +556,8 @@ msgid "eMail (will be your username)"
 msgstr "e-mail (sera votre nom d'utilisateur)"
 
 #: agrammon/module/user/Account.js
-msgid "Password (minimum 6 characters)"
-msgstr "Mot de passe (min. 6 caractères)"
+msgid "Password (minimum 8 characters)"
+msgstr ""
 
 #: agrammon/module/user/Account.js
 msgid "Repeat Password"
@@ -591,11 +587,11 @@ msgstr "Changer de mot de passe"
 msgid "Passwords must be equal."
 msgstr "Les mots de passe doivent être égaux."
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "An activation key will be sent to you by eMail after pressing the button"
 msgstr "Une clé d'activation va vous être envoyée par email en cliquant sur le bouton"
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Key (sent by eMail)"
 msgstr "Clé (envoyée par e-mail)"
 
@@ -619,7 +615,7 @@ msgstr "Réinitialisation du mot de passe"
 msgid "Login"
 msgstr "Entrer"
 
-#: agrammon/ui/menu/AdminMenu.js
+#: agrammon/module/user/Account.js
 msgid "Reset password"
 msgstr "Changer de mot de passe"
 
@@ -627,11 +623,11 @@ msgstr "Changer de mot de passe"
 msgid "Activate new account"
 msgstr "Activer un nouveau compte"
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Enter key below to re-activate your account"
 msgstr "Entrer la clé ci-dessous pour réactiver votre compte"
 
-#: agrammon/module/user/Account.js
+#. NO LONGER USED
 msgid "Enter key below to activate your account"
 msgstr "Entrer la clé ci-dessous pour activer votre compte"
 
@@ -935,7 +931,7 @@ msgstr ""
 msgid "Error %1: %2 - please login again"
 msgstr ""
 
-#: agrammon/module/user/Login.js
+#. NO LONGER USED
 msgid "Remember"
 msgstr ""
 
@@ -1018,3 +1014,27 @@ msgstr ""
 #: agrammon/io/remote/Rest.js
 msgid "Error in communication with server"
 msgstr ""
+
+#: agrammon/module/user/Account.js
+msgid "Password must have at least 8 characters."
+msgstr ""
+
+#: agrammon/module/user/Account.js
+msgid "An activation link will be sent to you by eMail after pressing the button"
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "An activation link will be sent to you by eMail."
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "Account creation successful"
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "Account activation key sent to %1"
+msgstr ""
+
+#: agrammon/module/user/SelfService.js
+msgid "A confirmation link will be sent to you by eMail."
+msgstr ""
diff --git a/lib/Agrammon/DB.rakumod b/lib/Agrammon/DB.rakumod
index 3c4bf054e..34eae0cb8 100644
--- a/lib/Agrammon/DB.rakumod
+++ b/lib/Agrammon/DB.rakumod
@@ -4,17 +4,18 @@ role Agrammon::DB {
     method connection() { $*AGRAMMON-DB-CONNECTION }
 
     method with-db(&operation) {
-        with $*AGRAMMON-DB-HANDLE {
-            operation($*AGRAMMON-DB-HANDLE);
+        with $*AGRAMMON-DB-CONNECTION {
+            operation($*AGRAMMON-DB-CONNECTION);
         }
         else {
+            note "Using fresh DB handle";
             self!with-fresh-handle(&operation);
         }
     }
 
     method !with-fresh-handle(&operation) {
         my $handle = self.connection.db;
-        my $*AGRAMMON-DB-HANDLE = $handle;
+        my $*AGRAMMON-DB-CONNECTION = $handle;
         my \result := operation($handle);
         $handle.finish;
         return result;
diff --git a/lib/Agrammon/DB/Dataset.rakumod b/lib/Agrammon/DB/Dataset.rakumod
index 56ded93a2..875841105 100644
--- a/lib/Agrammon/DB/Dataset.rakumod
+++ b/lib/Agrammon/DB/Dataset.rakumod
@@ -496,6 +496,11 @@ class Agrammon::DB::Dataset does Agrammon::DB::Variant {
                 RETURNING data_val
             SQL
 
+            CATCH {
+                "Couldn't store variable $variable";
+                .note;
+            }
+
             # couldn't store variable
             die X::Agrammon::DB::Dataset::StoreDataFailed.new($variable) unless $ret.rows;
 
diff --git a/lib/Agrammon/DB/User.rakumod b/lib/Agrammon/DB/User.rakumod
index 5823e4035..05a1ce5f2 100644
--- a/lib/Agrammon/DB/User.rakumod
+++ b/lib/Agrammon/DB/User.rakumod
@@ -1,6 +1,7 @@
 use v6;
 
 use Crypt::Random;
+use Crypt::Random::Extra;
 use Digest::SHA1::Native;
 
 use Agrammon::DB;
@@ -38,6 +39,12 @@ class X::Agrammon::DB::User::CreateFailed is Exception {
     }
 }
 
+class X::Agrammon::DB::User::ActivationFailed is Exception {
+    method message {
+        "Couldn't activate account";
+    }
+}
+
 class X::Agrammon::DB::User::NoPassword is Exception {
     method message() {
         "Need password to create an account!";
@@ -57,12 +64,18 @@ class X::Agrammon::DB::User::PasswordResetFailed is Exception {
     }
 }
 
-class X::Agrammon::DB::User::InvalidPassword is Exception {
+class X::Agrammon::DB::User::InvalidLogin is Exception {
     method message() {
         "Invalid username or password";
     }
 }
 
+class X::Agrammon::DB::User::InvalidPassword is Exception {
+    method message() {
+        "Password does not fit requirements";
+    }
+}
+
 class X::Agrammon::DB::User::UnknownUser is Exception {
     has Str $.username is required;
     method message() {
@@ -103,11 +116,13 @@ class Agrammon::DB::User does Agrammon::DB {
     }
 
     my $secret = crypt_random(Int(64/8));
+    my $encrypt-key = 'jah7Eiyitui1Zibe';
 
     sub get-password-key($username, $password) {
         my $digest = sha1-hex($username ~ $password ~ $secret);
+        return $digest;
         # only easily human readable characters
-        return substr($digest ~~ tr/1/x/, 5, 6);
+        # return substr($digest ~~ tr/1/x/, 5, 6);
     }
 
     method get-account-key() {
@@ -121,6 +136,7 @@ class Agrammon::DB::User does Agrammon::DB {
         my $role = $role-name || 'user';
         die X::Agrammon::DB::User::Exists.new(:$!username) if self.exists;
         die X::Agrammon::DB::User::NoUsername.new(:$!username) unless $!username;
+        die X::Agrammon::DB::User::InvalidPassword.new unless password-allowed($!password);
 
         self.with-db: -> $db {
             my $ret = $db.query(q:to/SQL/, $role);
@@ -136,8 +152,42 @@ class Agrammon::DB::User does Agrammon::DB {
 
             $ret = $db.query(q:to/SQL/, $!username, $!firstname, $!lastname, $!password, $!organisation, %r );
                 INSERT INTO pers (pers_email, pers_first, pers_last,
-                                  pers_password, pers_org, pers_role)
-                VALUES ($1, $2, $3, crypt($4, gen_salt('bf')), $5, $6)
+                                  pers_password, pers_org, pers_role, pers_activated)
+                VALUES ($1, $2, $3, crypt($4, gen_salt('bf')), $5, $6, now())
+                RETURNING pers_id
+            SQL
+
+            die X::Agrammon::DB::User::CreateFailed.new(:$!username) unless $ret.rows;
+        }
+        return self;
+    }
+
+    # we set a random password (must be NOT NULL)
+    # and save the encrypted password and the activation key
+    method self-create-account($role-name) {
+        my $role = $role-name || 'user';
+        die X::Agrammon::DB::User::Exists.new(:$!username) if self.exists;
+        die X::Agrammon::DB::User::NoUsername.new(:$!username) unless $!username;
+        die X::Agrammon::DB::User::InvalidPassword.new unless password-allowed($!password);
+
+        my $key;
+
+        self.with-db: -> $db {
+            my $ret = $db.query(q:to/SQL/, $role);
+                SELECT role_id   AS id,
+                       role_name AS name
+                  FROM role
+                 WHERE role_name = $1
+            SQL
+            die X::Agrammon::DB::User::UnknownRole.new(:$role) unless $ret.rows;
+
+            my %r = $ret.hash;
+            $!role = Agrammon::DB::Role.new(|%r);
+            $key = self.get-account-key();
+            $ret = $db.query(q:to/SQL/, $!username, $!firstname, $!lastname, $!organisation, %r, $!password, $encrypt-key, $key );
+                INSERT INTO pers (pers_email, pers_first, pers_last,
+                                  pers_password, pers_org, pers_role, pers_newpassword, pers_newpassword_key)
+                VALUES ($1, $2, $3, gen_random_uuid(), $4, $5, encode(encrypt($6, $7, 'aes'), 'base64'), $8)
                 RETURNING pers_id
             SQL
 
@@ -145,6 +195,25 @@ class Agrammon::DB::User does Agrammon::DB {
 
             $!id = $ret.value;
         }
+        return $key;
+    }
+
+    # set the real password
+    method activate-account($key) {
+        # note "User: Activating account with key $key";
+        self.with-db: -> $db {
+            my $ret = $db.query(q:to/SQL/, $key, $encrypt-key, DateTime.now);
+                UPDATE pers SET pers_password = crypt(convert_from(decrypt(decode(pers_newpassword, 'base64'), $2, 'aes'), 'UTF-8'), gen_salt('bf')),
+                                pers_activated = $3,
+                                pers_password_changed = $3,
+                                pers_newpassword = NULL, pers_newpassword_key = NULL
+                 WHERE pers_newpassword_key = $1
+                RETURNING pers_email
+            SQL
+
+            $!username = $ret.value if $ret.rows;
+        }
+        self.load if $!username;
         return self;
     }
 
@@ -210,8 +279,9 @@ class Agrammon::DB::User does Agrammon::DB {
     method change-password($old, $new) {
         self.with-db: -> $db {
 
-            die X::Agrammon::DB::User::InvalidPassword.new    unless self.password-is-valid($!username, $old);
+            die X::Agrammon::DB::User::InvalidLogin.new    unless self.password-is-valid($!username, $old);
             die X::Agrammon::DB::User::PasswordsIdentical.new if $old eq $new;
+            die X::Agrammon::DB::User::InvalidPassword.new unless password-allowed($new);
 
             $db.query(q:to/SQL/, $!username, $new);
                 UPDATE pers
@@ -220,7 +290,7 @@ class Agrammon::DB::User does Agrammon::DB {
                 RETURNING pers_email
             SQL
 
-            die X::Agrammon::DB::User::InvalidPassword.new unless self.password-is-valid($!username, $new);
+            die X::Agrammon::DB::User::InvalidLogin.new unless self.password-is-valid($!username, $new);
         }
     }
 
@@ -228,11 +298,18 @@ class Agrammon::DB::User does Agrammon::DB {
         get-password-key($username, $password) eq $key
     }
 
+    sub password-allowed(Str $password) {
+        return False if $password.chars < 8;
+        return True;
+    }
+
     method reset-password($email, $password, $key?) {
         # self reset, anonymous user
         if $key and not password-key-is-valid($email, $password, $key) {
             die X::Agrammon::DB::User::CannotResetPassword.new;
         }
+        die X::Agrammon::DB::User::InvalidPassword.new unless password-allowed($password);
+
         self.with-db: -> $db {
             $db.query(q:to/SQL/, $email, $password);
                 UPDATE pers
@@ -244,4 +321,26 @@ class Agrammon::DB::User does Agrammon::DB {
         }
     }
 
+    method self-reset-password($new-password) {
+        die X::Agrammon::DB::User::InvalidPassword.new unless password-allowed($new-password);
+
+        # self reset, anonymous user
+        my $email = $!username;
+        my $key = self.get-account-key();
+
+        self.with-db: -> $db {
+            my $ret = $db.query(q:to/SQL/, $email, $new-password, $encrypt-key, $key);
+                UPDATE pers
+                   SET pers_newpassword = encode(encrypt($2, $3, 'aes'), 'base64'),
+                       pers_newpassword_key = $4
+                 WHERE pers_email = $1
+                RETURNING pers_email
+                SQL
+
+            die X::Agrammon::DB::User::PasswordResetFailed.new unless $ret.rows;
+            note "User: confirmation key=$key";
+        }
+        return $key;
+    }
+
 }
diff --git a/lib/Agrammon/Model.rakumod b/lib/Agrammon/Model.rakumod
index 2c1cbe414..24ebe4dd3 100644
--- a/lib/Agrammon/Model.rakumod
+++ b/lib/Agrammon/Model.rakumod
@@ -174,7 +174,7 @@ class Agrammon::Model {
             if $gui-root-module {
                 # what a silly inline signaling
                 my @g = $gui-root-module.gui.split(',');
-                %gui-root = :de(@g[1]), :fr(@g[2]), :en(@g[3] // @g[0]);
+                %gui-root = :de(@g[1]), :fr(@g[2]), :en(@g[3] // @g[0]), :raw(@g[0]);
             }
             if $!module.is-multi {
                 for $input-data.inputs-list-for($tax) -> $multi-input {
diff --git a/lib/Agrammon/OutputFormatter/CollectData.rakumod b/lib/Agrammon/OutputFormatter/CollectData.rakumod
index 1c66fb31d..d31d3056b 100644
--- a/lib/Agrammon/OutputFormatter/CollectData.rakumod
+++ b/lib/Agrammon/OutputFormatter/CollectData.rakumod
@@ -14,7 +14,7 @@ sub collect-data(
     # add inputs
     my @inputs;
     for $model.annotate-inputs($inputs) -> $ai {
-        my $gui = $ai.gui-root{$language} // 'NO GUI ROOT';
+        my $gui-translated = $ai.gui-root{$language} // 'NO GUI ROOT';
         my $value = $ai.value;
         my $value-translated = $value;
         if $value and $ai.input.enum {
@@ -23,11 +23,13 @@ sub collect-data(
         @inputs.push( %(
             :module($ai.module.taxonomy),
             :instance($ai.instance-id // ''),
-            :input($ai.input.labels{$language} // $ai.input.labels // $ai.input.name),
+            :input($ai.input.name),
+            :input-translated($ai.input.labels{$language} // $ai.input.labels // $ai.input.name),
             :$value,
             :$value-translated,
             :unit($ai.input.units{$language} // $ai.input.units // ''),
-            :$gui,
+            :$gui-translated,
+            :gui($ai.gui-root{'raw'}),
         ));
     }
 
diff --git a/lib/Agrammon/OutputFormatter/ExcelFast.pm6 b/lib/Agrammon/OutputFormatter/ExcelFast.pm6
index 8bfb55699..ccfa01d72 100644
--- a/lib/Agrammon/OutputFormatter/ExcelFast.pm6
+++ b/lib/Agrammon/OutputFormatter/ExcelFast.pm6
@@ -8,6 +8,7 @@ use Agrammon::Web::SessionUser;
 #use Spreadsheet::XLSX;
 #use Spreadsheet::XLSX::Styles;
 
+use Temp::Path;
 use Excel::Writer::XLSX:from;
 
 sub input-output-as-excel(
@@ -19,9 +20,8 @@ sub input-output-as-excel(
     Bool $include-filters, Bool $all-filters
 ) is export {
 
-    my $temp-filename = "$dataset-name.xlsx";
-
-    my $workbook = Excel::Writer::XLSX.new($temp-filename);
+    my $temp-file = make-temp-path :suffix<.xlsx>;
+    my $workbook = Excel::Writer::XLSX.new($temp-file.absolute);
     
     # prepare sheets
     my $output-sheet = $workbook.add_worksheet('Ergebnisse');
@@ -162,5 +162,5 @@ sub input-output-as-excel(
         $row-formatted++;
     }
     $workbook.close();
-    return $temp-filename.IO.slurp: :bin;
+    return $temp-file.slurp: :bin;
 }
diff --git a/lib/Agrammon/OutputFormatter/ExcelFast.rakumod b/lib/Agrammon/OutputFormatter/ExcelFast.rakumod
new file mode 100644
index 000000000..1d8d14bf3
--- /dev/null
+++ b/lib/Agrammon/OutputFormatter/ExcelFast.rakumod
@@ -0,0 +1,187 @@
+use v6;
+use Agrammon::Config;
+use Agrammon::Model;
+use Agrammon::Outputs;
+use Agrammon::OutputFormatter::CollectData;
+use Agrammon::Timestamp;
+use Agrammon::Web::SessionUser;
+#use Spreadsheet::XLSX;
+#use Spreadsheet::XLSX::Styles;
+
+use Temp::Path;
+use Excel::Writer::XLSX:from;
+
+sub input-output-as-excel(
+    Agrammon::Config $cfg,
+    $user,
+    Str $dataset-name, Agrammon::Model $model,
+    Agrammon::Outputs $outputs, Agrammon::Inputs $inputs, $reports,
+    Str $language, Int $report-selected,
+    Bool $include-filters, Bool $all-filters
+) is export {
+
+    my $temp-file = make-temp-path :suffix<.xlsx>;
+    my $workbook = Excel::Writer::XLSX.new($temp-file.absolute);
+    
+    # prepare sheets
+    my $output-sheet = $workbook.add_worksheet('Ergebnisse');
+    my $output-sheet-formatted = $workbook.add_worksheet('Ergebnisse formatiert');
+    my $input-sheet = $workbook.add_worksheet('Eingaben');
+    my $input-sheet-formatted = $workbook.add_worksheet('Eingaben formatiert');
+    my $input-sheet-raw = $workbook.add_worksheet('Eingaben für REST');
+    
+    my $timestamp = timestamp;
+    my $model-version = $cfg.gui-title{$language} ~ " - " ~ $cfg.gui-variant;
+
+    # set column width
+    for ($output-sheet, $output-sheet-formatted) -> $sheet {
+        $sheet.set_column(0, 0, 20);
+        $sheet.set_column(1, 1, 32);
+        $sheet.set_column(2, 2, 20);
+        $sheet.set_column(3, 3, 10);
+
+    }
+
+    $input-sheet.set_column(0, 0, 30);
+    $input-sheet.set_column(1, 1, 20);
+    $input-sheet.set_column(2, 3, 50);
+    $input-sheet.set_column(4, 4, 10);
+
+    $input-sheet-raw.set_column(0, 0, 60);
+    $input-sheet-raw.set_column(1, 1, 50);
+    $input-sheet-raw.set_column(2, 2, 50);
+
+    $input-sheet-formatted.set_column(0, 0, 10);
+    $input-sheet-formatted.set_column(1, 2, 50);
+    $input-sheet-formatted.set_column(3, 3, 10);
+
+    my $bold-format = $workbook.add_format();
+    $bold-format.set_bold();
+
+    my $number-format = $workbook.add_format();
+    $number-format.set_num_format( '0.000' );
+
+    my $number-format-short = $workbook.add_format();
+    $number-format-short.set_num_format( '0.0' );
+
+    my $number-format-right = $workbook.add_format();
+    $number-format-right.set_num_format( '0.000' );
+    $number-format-right.set_align( 'right' );
+
+    my $number-format-right-short = $workbook.add_format();
+    $number-format-right-short.set_num_format( '0.0' );
+    $number-format-right-short.set_align( 'right' );
+
+    for ($output-sheet-formatted, $input-sheet-formatted) -> $sheet {
+        $sheet.write(0, 0, $dataset-name, $bold-format);
+        $sheet.write(1, 0, $user.username);
+        $sheet.write(2, 0, $model-version);
+        $sheet.write(3, 0, $timestamp);
+    }
+
+    for ($output-sheet, $input-sheet) -> $sheet {
+        $sheet.write(0, 0, $dataset-name);
+        $sheet.write(1, 0, $user.username);
+        $sheet.write(2, 0, $model-version);
+        $sheet.write(3, 0, $timestamp);
+    }
+    $input-sheet-raw.write(0,0, "# $dataset-name, {$user.username}, $model-version, $timestamp");
+
+    # prepared data
+    my %data = collect-data(
+        $model,
+        $outputs, $inputs, $reports,
+        $language, $report-selected,
+        $include-filters, $all-filters
+    );
+
+    my @records;
+    # TODO: fix sorting
+    my $col = 0;
+    my $row = 5;
+    my $row-formatted = $row;
+    my $row-raw = 1;
+    my $last-print = '';
+#    for @records.sort(+*.) -> %rec {
+    my $last-instance = '';
+    my $last-module = '';
+    @records := %data;
+    note "inputs: " ~ @records.elems if %*ENV;
+    for @records -> %rec {
+
+        # unformatted data
+        $input-sheet.write($row, $col+0, %rec);
+        $input-sheet.write($row, $col+1, %rec);
+        $input-sheet.write($row, $col+2, %rec);
+        $input-sheet.write($row, $col+3, (%rec // '???'), $number-format-right);
+        $input-sheet.write($row, $col+4, %rec);
+        $row++;
+
+        # formatted data
+        my $instance = %rec;
+        my $module = %rec;
+        if $module ne $last-module {
+            $input-sheet-formatted.write($row-formatted, $col+0, $module, $bold-format);
+            $row-formatted++;
+            $last-module = $module;
+        }
+        if $instance and $instance ne $last-instance {
+            $input-sheet-formatted.write($row-formatted, $col+1, $instance, $bold-format);
+            $row-formatted++;
+            $last-instance = $instance;
+        }
+        $input-sheet-formatted.write($row-formatted, $col+1, %rec);
+        $input-sheet-formatted.write($row-formatted, $col+2, (%rec // '???'), $number-format-right-short);
+        $input-sheet-formatted.write($row-formatted, $col+3, %rec);
+        $row-formatted++;
+
+        # raw data
+        my $module-instance = %rec;
+        my $gui = %rec;
+        if $instance {
+#            note "instance=$instance, gui=$gui, module-instance=$module-instance";
+            my $match = $module-instance.match(/$gui/);
+            $module-instance = $match.replace-with("$gui\[$instance\]");
+        }
+        $input-sheet-raw.write($row-raw, $col+0, $module-instance);
+        $input-sheet-raw.write($row-raw, $col+1, %rec);
+        $input-sheet-raw.write($row-raw, $col+2, (%rec // '???'), $number-format-right);
+        $row-raw++;
+
+    }
+
+    # add outputs
+    my %print-labels = %data;
+
+    @records := %data;
+    note "outputs: " ~ @records.elems if %*ENV;
+    $row = 5;
+    $row-formatted = $row;
+    $col = 0;
+    $last-print = '';
+
+    for @records.sort(+*.) -> %rec {
+        my $print = %rec; # can be undefined or empty
+        $output-sheet.write($row, $col+0, %print-labels{$print}{$language} // '') if $print;
+        $output-sheet.write($row, $col+1, %rec