diff --git a/doc/HTML.md b/doc/HTML.md index d7d3266f03759c60187b40e35e44d6675f1b4863..2fd09e25523f921b14765800f42a4def042834dc 100644 --- a/doc/HTML.md +++ b/doc/HTML.md @@ -37,3 +37,31 @@ call to `api/load.php` upon change. ### id="close-button" ### id="delete-button" ### id="form-new-button" + +## Typeahead + +Typeahead capable text input elements will be defined by the following attributes: + +### .class='qfq-typeahead' + +### .data-typeahead-sip + +The SIP will store: + +Use with SQL: `typeAheadSql` + +Use with LDAP: +* `typeAheadLdapServer` +* `typeAheadLdapBaseDn` +* `typeAheadLdapSearch` +* `typeAheadLdapValuePrintf` +* `typeAheadLdapKeyPrintf` + +### .data-typeahead-limit + +* Defines the limit of entries shown on the client. Default on client is 5. The server will always send a value. The server default is 20. + +### .data-typeahead-minlength + +* Defines the string minlegth, typed by the user, before the first lookup is started. Default is 2. + diff --git a/doc/NewVersion.md b/doc/NewVersion.md index 895015a74c867768a00c4a8a3bb043a381bd1d9c..deae07d6d7cc8858ba944342fb1090ce02c74e1d 100644 --- a/doc/NewVersion.md +++ b/doc/NewVersion.md @@ -2,31 +2,33 @@ Neue Versionsnummer =================== -1) In folgenden Files anpassen: +1) Die aktuellen Commits anschauen und wichtige Topics uebernehmen (git log > ~/qfq.log, alles bis zum letzten TAG anschauen): + + * qfq/extension/Documentation/5_Release.rst + * Den Inhalt von 5_Release.txt kopieren nach qfq/extension/RELEASE.txt. + +2) In folgenden Files anpassen: + * extension/Documentation/_make/conf.py: release, version * extension/Documentation/Settings.yml: version * extension/ext_emconf.php: version - * extension/RELEASE.txt -2) Im Projektverzeichnis: +3) Im Projektverzeichnis: make t3sphinx (dadurch fallen Fehler in RESTdoc Syntax auf) - -3) Merge auf master Branch +4) Neues ZIP bauen: make qfq.zip + +5) Merge auf master Branch git checkout master git merge crose_work -4) Neuen Tag vergeben: git tag 0.12.0 +6) Neuen Tag vergeben: git tag 0.14.0 -5) Alle Files, inkl. Tags, in GIT einchecken. +7) Alle Files, inkl. Tags, in GIT einchecken. -6) Per PhpStorm Sync aller Files auf VM qfq +8) Per PhpStorm Sync aller Files auf VM qfq -7) In T3 Instanz Dokumentation rendern lassen. - +9) In T3 Instanz Dokumentation rendern lassen. T3 6.2: Admin Tools > Extension Manager > QFQ > Doku HTML: rechts oben 'Render Documentation' - - - diff --git a/doc/PROTOCOL.md b/doc/PROTOCOL.md index 2cd25c77d334e05dec0b645e1df1e19d39830cd2..2f788733ae86197a660ada4c5a6df95946958096 100644 --- a/doc/PROTOCOL.md +++ b/doc/PROTOCOL.md @@ -207,6 +207,20 @@ The format of redirect information is outlined below : Used to provide an URL when `"redirect"` is set to `"url"`. It should be disregarded unless `"redirect"` is set to `"url"`. +### Typeahead dict Response + + { + ... + [ + { + "key": "<key value>", + "value": <display value> + }, + ... + ], + ... + } + ## API Endpoints @@ -321,6 +335,22 @@ Server Response : The response contains a [Minimal Response]. [Redirection Response] may be included. +### Typeahead + +The Client initiates Typeahead actions via a GET request. A JSON key/value dict will we be send back as response. +The Client GET request contains a 'sip' and the already typed value as 'query' paramter. + +Request URL +: api/typeahead.php + +Request Method +: GET + +URL Parameters +: `sip`, `query` + +Server Response +: The response contains at least a [Minimal Response]. In addition, a [Typeahead dict], ## Glossary diff --git a/extension/Documentation/AdministratorManual/Index.rst b/extension/Documentation/AdministratorManual/Index.rst deleted file mode 100644 index e6384cffcc56ee9ded663c41b1e328ba31cf9dad..0000000000000000000000000000000000000000 --- a/extension/Documentation/AdministratorManual/Index.rst +++ /dev/null @@ -1,250 +0,0 @@ -.. ================================================== -.. FOR YOUR INFORMATION -.. -------------------------------------------------- -.. -*- coding: utf-8 -*- with BOM. - -.. include:: ../Includes.txt - - -.. _admin-manual: - -Administrator Manual -==================== - -Preparation ------------ - -Report & Form -^^^^^^^^^^^^^ - -The QFQ extension needs in PHP 5.x the PHP MySQL native driver. The following functions are used and are only available with the -native driver (see also: http://dev.mysql.com/downloads/connector/php-mysqlnd/): - -* mysqli::get_result (important), -* mysqli::fetch_all (nice to use) - -To normalize UTF8 input, the *php5-intl* resp. *php7.0-intl* package is needed by - -* normalizer::normalize() - -Preparation for Ubuntu 14.04:: - - sudo apt-get install php5-mysqlnd php5-intl - sudo php5enmod mysqlnd - sudo service apache2 restart - -Preparation steps for Ubuntu 16.04:: - - sudo apt install php7.0-intl - -Print page via wkhtmltopdf -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Different browser prints the same page in different variations. To prevent this, QFQ implements a small PHP wrapper `print.php` -which uses http://wkhtmltopdf.org/ (webkit based) to convert HTML to PDF. The converter is not included in QFQ and has -to be manually installed. - -Hint: The Ubuntu package `wkhtmltopdf` needs a running Xserver - this does not work on a headless webserver. Best is to -install the QT version from the named website above. - -In config.qfq.ini specify the: -* installed `wkhtmltopdf` binary, -* the site base URL. - -Provide a `print this page`-link (replace {current pageId}):: - - <a href="typo3conf/ext/qfq/qfq/api/print.php?id={current pageId}">Print this page</a> - -Any parameter specified after `print.php` will be delivered to `wkhtmltopdf` as part of the URL. - -Typoscript code to implement a print link on every page:: - - 10 = TEXT - 10 { - wrap = <a href="typo3conf/ext/qfq/qfq/api/print.php?id=|&type=2"><span class="glyphicon glyphicon-print" aria-hidden="true"></span> Printview</a> - data = page:uid - } - -Setup ------ - -* Install the extension via the Extensionmanager. - - * If you install the extension by manual download/upload and get an error message - "can't activate extension": rename the downloaded zip file to `qfq.zip` or `qfq_<version>.zip` (e.g. version: 0.9.1). - - * If the Extensionmanager stops after importing: check your memory limit in php.ini. - -* Enable the online Documentation_. -* Copy/rename the file *<Documentroot>/typo3conf/ext/<ext_dir>/config.example.qfq.ini* to - *<Documentroot>/typo3conf/config.qfq.ini* and configure the necessary values: `config.qfq.ini`_ - The configuration file is outside the extension directory to not loose it during updates. -* Play the SQL File *<ext_dir>/qfq/sql/formEditor.sql* to fill the database with the *FormEditor* records. -* Configure Typoscript to include Bootstrap, jQuery, QFQ javascript and CSS files. - -:: - - page.meta { - X-UA-Compatible = IE=edge - X-UA-Compatible.attribute = http-equiv - viewport=width=device-width, initial-scale=1 - } - - page.includeCSS { - - file1 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap.min.css - file2 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap-theme.min.css - file3 = typo3conf/ext/qfq/Resources/Public/Css/jqx.base.css - file4 = typo3conf/ext/qfq/Resources/Public/Css/jqx.bootstrap.css - file5 = typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css - } - - page.includeJS { - - file1 = typo3conf/ext/qfq/Resources/Public/JavaScript/jquery.min.js - file2 = typo3conf/ext/qfq/Resources/Public/JavaScript/bootstrap.min.js - file3 = typo3conf/ext/qfq/Resources/Public/JavaScript/validator.min.js - file4 = typo3conf/ext/qfq/Resources/Public/JavaScript/jqx-all.js - file5 = typo3conf/ext/qfq/Resources/Public/JavaScript/globalize.js - file6 = typo3conf/ext/qfq/Resources/Public/JavaScript/tinymce.min.js - file7 = typo3conf/ext/qfq/Resources/Public/JavaScript/EventEmitter.min.js - file8 = typo3conf/ext/qfq/Resources/Public/JavaScript/qfq.min.js - } - - -FormEditor ----------- -Setup a *report* to manage all *forms*: Create a Typo3 page and insert a content record of type *qfq*. In the bodytext insert the following code: - -:: - - # If there is a form given by SIP: show - form={{form:S}} - - 10 { - # List of Forms: Do not show this list of forms if there is a form given by SIP. - # Table header. - sql = SELECT CONCAT('{{pageId:T}}&form=Form&') as Pagen, '#', 'Name', 'Title', 'Table' FROM (SELECT 1) AS fake WHERE '{{form:SE}}'='' - head = <table class="table table-hover"> - tail = </table> - rbeg = <thead><tr> - rend = </tr></thead> - fbeg = <th> - fend = </th> - - 10 { - # All forms - sql = SELECT CONCAT('{{pageId:T}}&form=Form&r=', f.id) as Pagee, f.id, f.name, f.title, f.tableName, CONCAT('form=Form&r=', f.id) as Paged FROM Form AS f ORDER BY f.name - rbeg = <tr> - rend = </tr> - fbeg = <td> - fend = </td> - } - } - -.. _config-qfq-ini: - -config.qfq.ini --------------- - -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| Keyword | Example | Description | -+=============================+=========================================+============================================================================+ -| DB_USER | DB_USER=qfqUser | Credentials configured in MySQL | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| DB_PASSWORD | DB_PASSWORD=12345678 | Credentials configured in MySQL | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| DB_SERVER | DB_SERVER=localhost | Hostname of MySQL Server | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| DB_NAME | DB_NAME=qfq_db | Database name | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| DB_NAME_TEST | DB_NAME_TEST=qfq_db_test | Used during development of QFQ | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| DB_INIT | DB_INIT=set names utf8 | Global init for using the database. | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| SQL_LOG | SQL_LOG=sql.log | Filename to log SQL commands: relative to <ext_dir> or absolute. | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| SQL_LOG_MODE | SQL_LOG_MODE=modify | *all*: every statement will be logged - this is a lot | -| | | *modify*: log only statements who change data | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| SHOW_DEBUG_INFO | SHOW_DEBUG_INFO=auto | Possible values: auto|yes|no. For 'auto': If a BE User is logged in, | -| | | debug information will be shown on the fronend. | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| CSS_LINK_CLASS_INTERNA L | CSS_LINK_CLASS_INTERNAL=internal | CSS class name of links which points to internal tagets | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| CSS_LINK_CLASS_EXTERNAL | CSS_LINK_CLASS_EXTERNAL=external | CSS class name of links which points to internal tagets | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| CSS_CLASS_QFQ_CONTAINER |CSS_CLASS_QFQ_CONTAINER=container | QFQ with own Bootstrap: 'container'. | -| | | QFQ already nested in Bootstrap of mainpage: <empty> | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| CSS_CLASS_QFQ_FORM_PILL |CSS_CLASS_QFQ_FORM_PILL=qfq-color-grey-1 | Wrap around title bar for pills: CSS Class, typically a background color | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| CSS_CLASS_QFQ_FORM_BODY |CSS_CLASS_QFQ_FORM_BODY=qfq-color-grey-2 | Wrap around formelements: CSS Class, typically a background color | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| DATE_FORMAT | DATE_FORMAT= yyyy-mm-dd | Possible options: yyyy-mm-dd, dd.mm.yyyy | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| FORM_DATA_PATTERN_ERROR |FORM_DATA_PATTERN_ERROR=please check pa. | Customizable error message used in validator.js. 'pattern' violation | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| FORM_DATA_REQUIRED_ERROR |FORM_DATA_REQUIRED_ERROR=missing value | Customizable error message used in validator.js. 'required' fields | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| FORM_DATA_MATCH_ERROR |FORM_DATA_MATCH_ERROR=type error | Customizable error message used in validator.js. 'match' retype mismatch | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| FORM_DATA_ERROR |FORM_DATA_ERROR=generic error | Customizable error message used in validator.js. 'no specific' given | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| FORM_BUTTON_ON_CHANGE_CLASS | FORM_BUTTON_ON_CHANGE_CLASS=alert-info btn-info | Color for save button after modification | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| BASE_URL_PRINT | BASE_URL_PRINT=http://example.com | URL where wkhtmltopdf will fetch the HTML (no parameter, those comes later)| -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ -| WKHTMLTOPDF | WKHTMLTOPDF=/usr/bin/wkhtmltopdf | Binary where to find wkhtmltopdf | -+-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ - - -Example: *typo3conf/config.qfq.ini* - -:: - - ; To get internal default values, inactivate the option by commenting (= ';') it. - DB_USER = qfqUser - DB_SERVER = localhost - DB_PASSWORD = 12345678 - DB_NAME = qfq_db - DB_INIT = set names utf8 - SQL_LOG = sql.log - SHOW_DEBUG_INFO = auto - CSS_LINK_CLASS_INTERNAL = internal - CSS_LINK_CLASS_EXT = external - ;CSS_CLASS_QFQ_CONTAINER = - ;CSS_CLASS_QFQ_FORM = - CSS_CLASS_QFQ_FORM_PILL = qfq-color-grey-1 - CSS_CLASS_QFQ_FORM_BODY = qfq-color-grey-2 - ;FORM_DATA_PATTERN_ERROR = - ;FORM_DATA_REQUIRED_ERROR = - ;FORM_DATA_MATCH_ERROR = - ;FORM_DATA_ERROR = - ;FORM_BS_LABEL_COLUMNS = 3 - ;FORM_BS_INPUT_COLUMNS = 6 - ;FORM_BS_NOTE_COLUMNS = 3 - BASE_URL_PRINT=http://example.com - WKHTMLTOPDF=/usr/bin/wkhtmltopdf - -Documentation -------------- - -To render the QFQ reST documentation: - -* Take care to have 'unzip' and 'Python setuptools' installed (necessary to run). - -Preparation for Ubuntu 16.04:: - - sudo apt install unzip python-setuptools python-pip - -* Install the extension "Sphinx Python Documentation Generator and Viewer" (sphinx). - - * Execute the update script (symbol 'two arrows as a circle' behind the extension name) - * Choose 'Sphinx 1.4.4' - click on 'Import'. - -* In the Exension Manager open the configuration dialog of the extension 'sphinx'. Activate the 'Sphinx 1.4.4' option and save it. -* On top of the browser window click on the 'question mark' to open the menu, choose 'Sphinx'. -* Show doumentation 'QFQ Extension' - -* If you have problems with the rendering, please check: http://mbless.de/blog/2015/01/26/sphinx-doc-installation-steps.html diff --git a/extension/Documentation/Index.rst b/extension/Documentation/Index.rst index 2d8675aaf8da1a1d2d876e06404b365254c28497..bac666e318dda4b3c8e2eb3a287e336a2c31dc6d 100644 --- a/extension/Documentation/Index.rst +++ b/extension/Documentation/Index.rst @@ -15,7 +15,7 @@ QFQ Extension .. only:: html :Classification: - qfq + qfq :Version: |release| @@ -25,8 +25,9 @@ QFQ Extension :Description: The extension offers support to: - a) create HTML Forms by clicking them together, - b) create reports based an SQL queries. The SQL can be nested and offers support for any kind of tags. + + * Create HTML Forms by clicking them together, + * Create reports based an SQL queries. The SQL can be nested and offers support for any kind of tags. :Keywords: Quick Form Query, Form, Report, SQL, Query, Generator. @@ -35,10 +36,10 @@ QFQ Extension 2017 :Author: - Carsten Rose, Rafael Ostertag + Carsten Rose, Rafael Ostertag :Email: - carsten.rose@math.uzh.ch, rafael.ostertag@math.uzh.ch + carsten.rose@math.uzh.ch, rafael.ostertag@math.uzh.ch :License: This document is published under the Open Publication License @@ -56,7 +57,5 @@ QFQ Extension .. toctree:: :maxdepth: 4 - Introduction/Index - AdministratorManual/Index - UsersManual/Index + Manual Links diff --git a/extension/Documentation/UsersManual/Index.rst b/extension/Documentation/Manual.rst similarity index 87% rename from extension/Documentation/UsersManual/Index.rst rename to extension/Documentation/Manual.rst index c766c13cc20f66f87c51796f347376676cd09840..8831b008be46ae9490e96b691354541e1a326d14 100644 --- a/extension/Documentation/UsersManual/Index.rst +++ b/extension/Documentation/Manual.rst @@ -13,35 +13,288 @@ .. .. -*- coding: utf-8 -*- with BOM. -.. include:: ../Includes.txt +.. include:: Includes.txt -.. _users-manual: -Users manual +.. _installation: + +Installation ============ -The QFQ extension is activated through tt-content records. One or more tt-content records per page are necessary to render -*forms*, *reports* (exports) or to perform *delete* and *save* commands submitted by a QFQ form. +Preparation +----------- + +Report & Form +^^^^^^^^^^^^^ -Features not implemented yet ----------------------------- +In PHP 5.x the QFQ extension needs the PHP MySQL native driver. The following functions are used and are only available with the +native driver (see also: http://dev.mysql.com/downloads/connector/php-mysqlnd/): -* Multi Forms +* mysqli::get_result (important), +* mysqli::fetch_all (nice to use) -QFQ content element +To normalize UTF8 input, the *php5-intl* resp. *php7.0-intl* package is needed by + +* normalizer::normalize() + +Preparation for Ubuntu 14.04:: + + sudo apt-get install php5-mysqlnd php5-intl + sudo php5enmod mysqlnd + sudo service apache2 restart + +Preparation steps for Ubuntu 16.04:: + + sudo apt install php7.0-intl + +Print page via wkhtmltopdf +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Different browser prints the same page in different variations. To prevent this, QFQ implements a small PHP wrapper `print.php` +which uses http://wkhtmltopdf.org/ (webkit based) to convert HTML to PDF. The converter is not included in QFQ and has +to be manually installed. + +Hint: The Ubuntu package `wkhtmltopdf` needs a running Xserver - this does not work on a headless webserver. Best is to +install the QT version from the named website above. + +In config.qfq.ini specify the: + +* installed `wkhtmltopdf` binary, +* the site base URL. + +Provide a `print this page`-link (replace {current pageId}):: + + <a href="typo3conf/ext/qfq/qfq/api/print.php?id={current pageId}">Print this page</a> + +Any parameter specified after `print.php` will be delivered to `wkhtmltopdf` as part of the URL. + +Typoscript code to implement a print link on every page:: + + 10 = TEXT + 10 { + wrap = <a href="typo3conf/ext/qfq/qfq/api/print.php?id=|&type=2"><span class="glyphicon glyphicon-print" aria-hidden="true"></span> Printview</a> + data = page:uid + } + +Setup +----- + +* Install the extension via the Extensionmanager. + + * If you install the extension by manual download/upload and get an error message + "can't activate extension": rename the downloaded zip file to `qfq.zip` or `qfq_<version>.zip` (e.g. version: 0.9.1). + + * If the Extensionmanager stops after importing: check your memory limit in php.ini. + +* Enable the online local-documentation_. +* Copy/rename the file *<Documentroot>/typo3conf/ext/<ext_dir>/config.example.qfq.ini* to + *<Documentroot>/typo3conf/config.qfq.ini* and configure the necessary values: `config.qfq.ini`_ + The configuration file is outside the extension directory to not loose it during updates. +* Play the SQL File *<ext_dir>/qfq/sql/formEditor.sql* to fill the database with the *FormEditor* records. +* Configure Typoscript to include Bootstrap, jQuery, QFQ javascript and CSS files. + +:: + + page.meta { + X-UA-Compatible = IE=edge + X-UA-Compatible.attribute = http-equiv + viewport=width=device-width, initial-scale=1 + } + + page.includeCSS { + + file1 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap.min.css + file2 = typo3conf/ext/qfq/Resources/Public/Css/bootstrap-theme.min.css + file3 = typo3conf/ext/qfq/Resources/Public/Css/jqx.base.css + file4 = typo3conf/ext/qfq/Resources/Public/Css/jqx.bootstrap.css + file5 = typo3conf/ext/qfq/Resources/Public/Css/qfq-bs.css + } + + page.includeJS { + + file1 = typo3conf/ext/qfq/Resources/Public/JavaScript/jquery.min.js + file2 = typo3conf/ext/qfq/Resources/Public/JavaScript/bootstrap.min.js + file3 = typo3conf/ext/qfq/Resources/Public/JavaScript/validator.min.js + file4 = typo3conf/ext/qfq/Resources/Public/JavaScript/jqx-all.js + file5 = typo3conf/ext/qfq/Resources/Public/JavaScript/globalize.js + file6 = typo3conf/ext/qfq/Resources/Public/JavaScript/tinymce.min.js + file7 = typo3conf/ext/qfq/Resources/Public/JavaScript/EventEmitter.min.js + file8 = typo3conf/ext/qfq/Resources/Public/JavaScript/qfq.min.js + } + + +FormEditor +---------- +Setup a *report* to manage all *forms*: Create a Typo3 page and insert a content record of type *qfq*. In the bodytext insert the following code: + +:: + + # If there is a form given by SIP: show + form={{form:S}} + + 10 { + # List of Forms: Do not show this list of forms if there is a form given by SIP. + # Table header. + sql = SELECT CONCAT('{{pageId:T}}&form=Form&') as Pagen, '#', 'Name', 'Title', 'Table' FROM (SELECT 1) AS fake WHERE '{{form:SE}}'='' + head = <table class="table table-hover"> + tail = </table> + rbeg = <thead><tr> + rend = </tr></thead> + fbeg = <th> + fend = </th> + + 10 { + # All forms + sql = SELECT CONCAT('{{pageId:T}}&form=Form&r=', f.id) as Pagee, f.id, f.name, f.title, f.tableName, CONCAT('form=Form&r=', f.id) as Paged FROM Form AS f ORDER BY f.name + rbeg = <tr> + rend = </tr> + fbeg = <td> + fend = </td> + } + } + +.. _config-qfq-ini: + +config.qfq.ini +-------------- + ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| Keyword | Example | Description | ++=============================+=========================================+============================================================================+ +| DB_USER | DB_USER=qfqUser | Credentials configured in MySQL | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_PASSWORD | DB_PASSWORD=12345678 | Credentials configured in MySQL | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_SERVER | DB_SERVER=localhost | Hostname of MySQL Server | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_NAME | DB_NAME=qfq_db | Database name | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_NAME_TEST | DB_NAME_TEST=qfq_db_test | Used during development of QFQ | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DB_INIT | DB_INIT=set names utf8 | Global init for using the database. | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| SQL_LOG | SQL_LOG=sql.log | Filename to log SQL commands: relative to <ext_dir> or absolute. | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| SQL_LOG_MODE | SQL_LOG_MODE=modify | *all*: every statement will be logged - this is a lot | +| | | *modify*: log only statements who change data | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| SHOW_DEBUG_INFO | SHOW_DEBUG_INFO=auto | Possible values: auto|yes|no. For 'auto': If a BE User is logged in, | +| | | debug information will be shown on the fronend. | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_LINK_CLASS_INTERNA L | CSS_LINK_CLASS_INTERNAL=internal | CSS class name of links which points to internal tagets | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_LINK_CLASS_EXTERNAL | CSS_LINK_CLASS_EXTERNAL=external | CSS class name of links which points to internal tagets | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_CLASS_QFQ_CONTAINER |CSS_CLASS_QFQ_CONTAINER=container | QFQ with own Bootstrap: 'container'. | +| | | QFQ already nested in Bootstrap of mainpage: <empty> | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_CLASS_QFQ_FORM_PILL |CSS_CLASS_QFQ_FORM_PILL=qfq-color-grey-1 | Wrap around title bar for pills: CSS Class, typically a background color | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| CSS_CLASS_QFQ_FORM_BODY |CSS_CLASS_QFQ_FORM_BODY=qfq-color-grey-2 | Wrap around formelements: CSS Class, typically a background color | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| DATE_FORMAT | DATE_FORMAT= yyyy-mm-dd | Possible options: yyyy-mm-dd, dd.mm.yyyy | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| FORM_DATA_PATTERN_ERROR |FORM_DATA_PATTERN_ERROR=please check pa. | Customizable error message used in validator.js. 'pattern' violation | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| FORM_DATA_REQUIRED_ERROR |FORM_DATA_REQUIRED_ERROR=missing value | Customizable error message used in validator.js. 'required' fields | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| FORM_DATA_MATCH_ERROR |FORM_DATA_MATCH_ERROR=type error | Customizable error message used in validator.js. 'match' retype mismatch | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| FORM_DATA_ERROR |FORM_DATA_ERROR=generic error | Customizable error message used in validator.js. 'no specific' given | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| FORM_BUTTON_ON_CHANGE_CLASS | FORM_BUTTON_ON_CHANGE_CLASS=alert-info btn-info | Color for save button after modification | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| BASE_URL_PRINT | BASE_URL_PRINT=http://example.com | URL where wkhtmltopdf will fetch the HTML (no parameter, those comes later)| ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ +| WKHTMLTOPDF | WKHTMLTOPDF=/usr/bin/wkhtmltopdf | Binary where to find wkhtmltopdf | ++-----------------------------+-----------------------------------------+----------------------------------------------------------------------------+ + + +Example: *typo3conf/config.qfq.ini* + +:: + + ; To get internal default values, inactivate the option by commenting (= ';') it. + DB_USER = qfqUser + DB_SERVER = localhost + DB_PASSWORD = 12345678 + DB_NAME = qfq_db + DB_INIT = set names utf8 + SQL_LOG = sql.log + SHOW_DEBUG_INFO = auto + CSS_LINK_CLASS_INTERNAL = internal + CSS_LINK_CLASS_EXT = external + ;CSS_CLASS_QFQ_CONTAINER = + ;CSS_CLASS_QFQ_FORM = + CSS_CLASS_QFQ_FORM_PILL = qfq-color-grey-1 + CSS_CLASS_QFQ_FORM_BODY = qfq-color-grey-2 + ;FORM_DATA_PATTERN_ERROR = + ;FORM_DATA_REQUIRED_ERROR = + ;FORM_DATA_MATCH_ERROR = + ;FORM_DATA_ERROR = + ;FORM_BS_LABEL_COLUMNS = 3 + ;FORM_BS_INPUT_COLUMNS = 6 + ;FORM_BS_NOTE_COLUMNS = 3 + BASE_URL_PRINT=http://example.com + WKHTMLTOPDF=/usr/bin/wkhtmltopdf + +.. _local-documentation: + +Local Documentation ------------------- -QFQ is used by configuring Typo3 content elements. Insert one or more QFQ content elements on a Typo3 page. -Specify column and language per content record as wished. +To render the QFQ reST documentation: -The title of the QFQ content element will not be rendered. It's only visible in the backend for orientation. +* Take care to have 'unzip' and 'Python setuptools' installed (necessary to run). + +Preparation for Ubuntu 16.04:: + + sudo apt install unzip python-setuptools python-pip + +* Install the extension "Sphinx Python Documentation Generator and Viewer" (sphinx). + + * Execute the update script (symbol 'two arrows as a circle' behind the extension name) + * Choose 'Sphinx 1.4.4' - click on 'Import'. + +* In the Exension Manager open the configuration dialog of the extension 'sphinx'. Activate the 'Sphinx 1.4.4' option and save it. +* On top of the browser window click on the 'question mark' to open the menu, choose 'Sphinx'. +* Show doumentation 'QFQ Extension' + +* If you have problems with the rendering, please check: http://mbless.de/blog/2015/01/26/sphinx-doc-installation-steps.html + +.. _concept: + +Concept +======= + +The QFQ extension is activated through `tt-content` records of type `QFQ`. One (or more) tt-content records per Typo3 +page are necessary to render *forms* and *reports*. + +Access privileges +----------------- + +The Typo3 FE Groups can be used to implement access privileges. Such groups are assigned to +* Typo3 FE users, +* Typo3 pages, +* and/or Typo3 content records (e.g. QFQ records). + +This will be used for general page structure privileges. + +A `record base` privileges controlling (e.g. which user can edit +which person record) will be implizit configured, by the way that records are viewable / editable (or not) through +SQL in the specifiq QFQ tt-content statements. + +Typo3 QFQ content element +------------------------- +Insert one or more QFQ content elements on a Typo3 page. Specify column and language per content record as wished. + +The title of the QFQ content element will not be rendered. It's only visible in the backend for orientation. QFQ Keywords (Bodytext) ^^^^^^^^^^^^^^^^^^^^^^^ - +-------------------+---------------------------------------------------------------------------------+ | Name | Explanation | +===================+=================================================================================+ @@ -108,34 +361,13 @@ Debug * *SHOW_DEBUG_INFO = yes* (BE session exist) * *SHOW_DEBUG_INFO = no* (no BE session) -Form ----- - -* Forms will be created by using the *QFQ Form Editor* on the Typo3 frontend (HTML form). -* The Formeditor itself consist of two predefined QFQ forms: *form* and *formElement* -* Every form consist of a) a *Form* record and b) multiple *FormElement* records. -* A form is assigned to a *table*. Such a table is called the *primary table* for this form. -* There are three types of forms which can roughly categorized into: - - * *Simple* form: the form acts on one record, stored in one table. - - * The form will create necessary SQL commands for insert, update and delete (only primary record) automatically. - - * *Advanced* form: the form acts on multiple records, stored in more than one table. - - * Fields of the primary table acts like a *simple* form, all other fields have to be specified with *addNupdate* records. - - * *Multi* form: the form acts simultanously on more than one record. All records use the same *FormElements*. - - * The *FormElements* are defined as a regular *simple* / or *advanced* form, plus a SQL Query, which selects and - iterates over all records. Those records will be loaded at the same time. .. _variables: -Variable (incl. mixed SQL Statement) ------------------------------------- +Variables +--------- -Most fields of a form specification might contain: +Most fields of a form or report specification might contain: * ''constants'' (=strings), this is the standard use case. * ''variables'' retrieved from the stores (see below), @@ -172,13 +404,13 @@ Most fields of a form specification might contain: * If no value is found, the value is an <empty string>. URL Parameter -------------- +^^^^^^^^^^^^^ * URL (=GET) Parameter can be used in *forms* and *reports* as variables. * If a value violates a parameter sanitize class, the value becomes an empty string. Escape ------- +^^^^^^ * Variables used in SQL Statements might cause trouble, if they contain single or double ticks. * Escaping of single or double is defined by the parameter <escape> (fourth parameter): @@ -191,7 +423,7 @@ Escape * Escaping is only necessary inside of SQL queries. Sanitize class --------------- +^^^^^^^^^^^^^^ * All values in Store *C* (Client) and store *F* (Form) will be sanitized: * All :ref:`predefined-variable-names` have a specific default sanitize class. For these variables, it's not necessary @@ -224,8 +456,8 @@ Sanitize class -Store / prio ------------- +Store +===== Only variables that are known in a specified store can be substituted. @@ -260,7 +492,7 @@ Only variables that are known in a specified store can be substituted. | | and will be used in an SQL statement | | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ | Y | :ref:`STORE_SYSTEM`: a) Database, b) helper vars for logging/debugging: | | - | | SYSTEM_SQL_RAW ... SYSTEM_FORM_ELEMENT_COLUMN, c) Any custom fields: CONTACT, HELP, .. | | + | | SYSTEM_SQL_RAW ... SYSTEM_FORM_ELEMENT_COLUMN, c) Any custom fields: CONTACT, HELP, ...| | +-----+----------------------------------------------------------------------------------------+----------------------------------------------------------------------------+ * Default *<prio>*: *FSRVD* - Form / SIP / Record / Vars / Table definition. @@ -291,7 +523,7 @@ Store: *FORM* - F +---------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ | Name | Explanation | +=================================+============================================================================================================================================+ - | <FormElement name> | Name of native *FormElement*. To get, exactly and only, the specified *FormElement* (for 'pId'): *{{pId:F}}* | + | <FormElement name> | Name of native *FormElement*. To get, exactly and only, the specified *FormElement* (for 'pId'): *{{pId:F}}* | +---------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------+ .. _STORE_SIP: @@ -333,7 +565,7 @@ Store: *RECORD* - R +------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+ | Name | Explanation | +========================+==================================================================================================================================================+ - | <column name> | Name of a column of the primary table (as defined in the current form). To get, exactly and only, the specified form *FormElement*: *{{pId:R}}* | + | <column name> | Name of a column of the primary table (as defined in the current form). To get, exactly and only, the specified form *FormElement*: *{{pId:R}}* | +------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+ .. _STORE_BEFORE: @@ -350,7 +582,7 @@ This store is handy to compare new and old values of a form. +------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+ | Name | Explanation | +========================+==================================================================================================================================================+ - | <column name> | Name of a column of the primary table (as defined in the current form). To get, exactly and only, the specified form *FormElement*: *{{pId:R}}* | + | <column name> | Name of a column of the primary table (as defined in the current form). To get, exactly and only, the specified form *FormElement*: *{{pId:R}}* | +------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+ .. _STORE_CLIENT: @@ -530,9 +762,33 @@ SQL Statement * This is only possible for the outermost SELECT. + +Form +==== + +* Forms will be created by using the *QFQ Form Editor* on the Typo3 frontend (HTML form). +* The Formeditor itself consist of two predefined QFQ forms: *form* and *formElement* +* Every form consist of a) a *Form* record and b) multiple *FormElement* records. +* A form is assigned to a *table*. Such a table is called the *primary table* for this form. +* There are three types of forms which can roughly categorized into: + + * *Simple* form: the form acts on one record, stored in one table. + + * The form will create necessary SQL commands for insert, update and delete (only primary record) automatically. + + * *Advanced* form: the form acts on multiple records, stored in more than one table. + + * Fields of the primary table acts like a *simple* form, all other fields have to be specified with *addNupdate* records. + + * *Multi* form: the form acts simultanously on more than one record. All records use the same *FormElements*. + + * The *FormElements* are defined as a regular *simple* / or *advanced* form, plus a SQL Query, which selects and + iterates over all records. Those records will be loaded at the same time. + + .. _form-main: -Form: main +Definition ---------- +-------------------------+----------------------------------------------------------+-----------------------------------------------------------------------------------------+ @@ -709,7 +965,7 @@ FormElements * Each *form* contains one or more *FormElement*. * The *FormElements* are divided in three categories: - + * :ref:`class-container` * :ref:`class-native` * :ref:`class-action` @@ -834,7 +1090,7 @@ Fields: |checkType | enum('min|max', 'pattern', | | | | 'number', 'email') | | +---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ -|checkPattern | 'regexp' |If $checkType=='pattern': pattern to match | +|checkPattern | 'regexp' |If $checkType=='pattern': pattern to match | +---------------+-----------------------------+---------------------------------------------------------------------------------------------------+ |onChange | string |List of *FormElement*-names of current form, separated by ', ', If one of the named *FormElements* | | | | change, reload own data / status / mode | @@ -887,6 +1143,8 @@ Attributes defined in the parameter field See also at specific *FormElement* definitions. ++------------------------+--------+----------------------------------------------------------------------------------------------------------+ +| Name | Type | Note | +------------------------+--------+----------------------------------------------------------------------------------------------------------+ | data-pattern-error | string | Pattern violation: Text for error message used for all FormElements of current form | +------------------------+--------+----------------------------------------------------------------------------------------------------------+ @@ -1048,13 +1306,13 @@ Checkboxes can be rendered in mode: * Each field key (or the corresponding value from the key/value pair) will be rendered right beside the checkbox. * *FormElement.parameter* - * *checkBoxMode*: multi + * *checkBoxMode* = multi * *itemList* - E.g.: * ``itemList=red,blue,orange`` * ``itemList=1:red,2:blue,3:orange`` - * ``itemList={{!SELECT id, value FROM someTable}}`` + * *FormElement.sql1* = ``{{!SELECT id, value FROM someTable}}`` * *FormElement.maxlength* - vertical or horizontal alignment: * Value: '', 0, 1 - The check boxes will be aligned vertical. @@ -1133,6 +1391,47 @@ Type: text * *retypeNote* =<text> (optional): The note of the second element. * Also check the :ref:`fe-parameter-attributes` *data-...-error* to customize error messages shown by the validator. + +Type Ahead +'''''''''' + +Activating `typeahead` functionality offers an instant lookup of data and displaying them to the user, while the user is +typing. A dropdown box offers the results. As datasource the regular SQL connection or a LDAP query can be used. +With every keystroke (starting from the *typeAheadMinLength* characters), the already typed value will be transmitted to +the server, the lookup will be performed and the result is displayed as the dropdown box. + +* *FormElement.parameter*: + + * *typeAheadLimit* = <number>. Max numbers of result records to be shown. Default is 20. + * *typeAheadMinLength* = <number>. Minimum length to type before the first lookup starts. + +Depending of the `typeahead` setup, the given FormElement will contain the displayed `value` or `key` (if a key/value dict is +configured). + +SQL +;;; + +* *FormElement.parameter*: + + * *typeAheadSql* = `SELECT ... AS 'key', ... AS 'value' WHERE name LIKE ? OR firstName LIKE ? LIMIT 100` + + * If there is only one column in the SELECT statement, that one will be used and there is no dict (key/value pair). + * The query will be fired as a 'prepared statement'. + * The value, typed by the user, will be replaced on all places where a `?` appears. + * All `?` will be automatically surrounded by '%'. Therefore wildcard search is implemented: `... LIKE '%<?>%' ...` + +LDAP +;;;; + +* *FormElement.parameter*: + + * *typeAheadLdapServer* = FQDN of the searched server. E.g.: `directory.uzh.ch` + * *typeAheadLdapBaseDn* = Base DN. E.g.: `ou=Addressbook,dc=uzh,dc=ch` + * *typeAheadLdapSearch* = LDAP search expression. E.g.: `(|(cn=*?*)(mail=*?*)(ou=*?*)(roomNumber=*?*)(telephoneNumber=*?*))` + * *typeAheadLdapValuePrintf* = regular printf expression, LDAP attributenames will be used as variablenames. Will be shown + in the dropdownbox. E.g.: `'%s / %s / %s', mail, roomNumber, telephoneNumber` + * *typeAheadLdapKeyPrintf* = Same as `ldapValuePrintf` - on save, these content will be saved. E.g.: `'%s', mail` + Type: editor ^^^^^^^^^^^^ @@ -1586,7 +1885,12 @@ Situation 2: master.id=slave.xId (1:n) Type: sendmail ^^^^^^^^^^^^^^ -* Send mail(s) after saving the record. +* Send mail(s) will be processed after: + + * saving the record , + * processing all uploads, + * processing `afterSave` action `FormElements`. + * *FormElement.value*: Body of the email. @@ -1643,18 +1947,20 @@ Examples Content of a select list '''''''''''''''''''''''' -* Slave FormElement 'interpret' is 'select'-list, depending of 'music': +* Slave FormElement 'interpret' is 'select'-list, depending of 'music' + :: - sql={{!SELECT name FROM interpret WHERE music={{music:FE:alnumx}} ORDER BY name}} + sql={{!SELECT name FROM interpret WHERE music={{music:FE:alnumx}} ORDER BY name}} Show / Hide a *FormElement* ''''''''''''''''''''''''''' * Slave 'interpret' is displayed only for 'pop': + :: - modeSql={{SELECT IF( '{{music:FR:alnumx}}'='pop' ,'show', 'hidden' }} + modeSql={{SELECT IF( '{{music:FR:alnumx}}'='pop' ,'show', 'hidden' }} .. _form-layout: @@ -1705,95 +2011,543 @@ the following (switch off all non named): * close row tag: `/row` , -Report -====== - -General -------- - -To display a report on any given TYPO3 page, create a content element of type 'QFQ Element' (plugin) on that page. +Best practice +------------- -A simple example -^^^^^^^^^^^^^^^^ +Central configured values +^^^^^^^^^^^^^^^^^^^^^^^^^ -Assume that the database has a table person with columns firstName and lastName. To create a simple list of all persons, we can do the following: -:: +Any variable in *config.qfq.ini* can be used by *{{<varname>:Y}}* in form or report statements. - 10.sql = SELECT id AS pId, CONCAT(firstName, " ", lastName, " ") AS name FROM person +E.g. -10 Stands for a *root level* of the report (see section `Structure`_). 10.sql defines a SQL query for this specific level. When the query is executed it will return a result having one single column name containing first and last name -separated by a space character. + TECHNICAL_CONTACT = jane.doe@example.net -The HTML output displayed on the page resulting from only this definition could look as follows: -:: +Could be used in an *FormElement.type* = sendmail with *parameter* setting *sendMailFrom={{TECHNICAL_CONTACT:Y}}*. - Marc MusterElton JohnSpeedy Gonzales +Debug Report +^^^^^^^^^^^^ -.. +Writing "report's" in the nested notation or long queries broken over several lines, might not interpreted as wished. +Best for debugging is to specify in the tt-content record:: + debugShowBodyText = 1 +Note: Debug information is only display if it's enabled in *config.ini* by + * *SHOW_DEBUG_INFO=yes* or + * *SHOW_DEBUG_INFO=auto* and logged in in the same Browser as a Typo3 backend user. -I.e., QFQ will simply output the content of the SQL result row after row for each single level. +More detailed error messages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -However, we can modify (wrap) the output by setting the values of various keys for each level: 10.rsep=<br/> for example tells QFQ to seperate the rows of the result by a HTML-line break. The final result in this case is: +If *SHOW_DEBUG_INFO* is enabled, a full stacktrace and variable contents are displayed in case of an error. -:: +Person search form +^^^^^^^^^^^^^^^^^^ - 10.sql = SELECT id AS personId, CONCAT(firstName, " ", lastName, " ") AS name FROM person - 10.sep = <br /> +QFQ content record:: -HTML output: -:: + # Creates a small form that redirects back to this page + 10 { + sql = SELECT '_' + head = <form action='#' method='get'><input type='hidden' name='id' value='{{pageId:T}}'>Search: <input type='text' name='search' value='{{search:CE:all}}'><input type='submit' value='Submit'></form> + } - Marc Muster<br />Elton John<br />Speedy Conzales<br /> + # SQL statement will find and list all the relevant forms + 20 { + sql = SELECT CONCAT('?detail&form=form&r=', f.id) AS _Pagee, f.id, f.name, f.title + FROM Form AS f + WHERE f.name LIKE '%{{search:CE:all}}%' + head = <table class='table'> + tail = </table> + rbeg = <tr> + rend = </tr> + fbeg = <td> + fend = </td> + } -.. +Form: compute next free 'ord' automatically +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Syntax ------- +Requirement: new records should automatically get the highest number plus 10 for their 'ord' value. Existing records +should not be altered. - All **root level queries** will be fired in the order specified by 'level' (Integer value). +Version 1 +''''''''' - For **each** row of a query (this means *all* queries), all subqueries will be fired once. +Compute the next 'ord' in advance in the subrecord field of the primary form. Submit that value to the new record +via SIP parameter to the secondary form. - * E.g. if the outer query selects 5 rows, and a nested query always select 3 rows, than the total number of rows are 5 x 3 = 15 rows. +On the secondary form: for 'new' records choose the computed value, for existing records leave the value +unchanged. - There is a set of **variables** that will get replaced before the SQL-Query gets executed: +* Primary form, `subrecord` *FormElement*, field `parameter`: set :: - Column values of the recent rows: {{<level>.<columnname>}} + detail=id:formId,{{SELECT '&', IFNULL(fe.ord,0)+10 FROM Form AS f LEFT JOIN *FormElement* AS fe ON fe.formId=f.id WHERE + f.id={{r:S0}} ORDER BY fe.ord DESC LIMIT 1}}:ord - Global variables: {{global.<name>}} - Variables from specific stores: {{<name>[:<store/s>[:<sanitize class>]]}} +* Secondary form, `ord` *FormElement*, field `value`: set - Current row index: {{<level>.line.count}} + :: - Total rows (num_rows for SELECT and SHOW, affected_rows for UPDATE and INSERT): {{<level>.line.total}} + `{{RS0}}`. - Last insert id for INSERT: {{<level>.line.insertId}} +Version 2 +''''''''' - See :ref:`variables` for a full list of all available variables. +Compute the next 'ord' as default value direct inside the secondary form. No change is needed for the primary form. - Be aware that line.count / line.total have to be known before the query is fired. E.g. `10.sql = SELECT {{10.line.count}}, ... WHERE {{10.line.count}} = ...` - won't work as expected. `{{10.line.count}}` can't be replaced before the query is fired, but will be replaced during processing the result! +* Secondary form, `ord` *FormElement*, field `value`: set `{{SELECT IF({{ord:R0}}=0, MAX(IFNULL(fe.ord,0))+10,{{ord:R0}}) FROM (SELECT 1) AS a LEFT JOIN FormElement AS fe ON fe.formId={{formId:S0}} GROUP BY fe.formId}}`. +Form: Person Wizard - firstname, city +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Different types of SQL queries are possible: SELECT, INSERT, UPDATE, DELETE, SHOW +Requirement: A form that displays the column 'firstname' from table 'Person' and 'city' from table 'Address'. If the +records not exist, the form should create it. - Only SELECT and SHOW queries will fire subqueries. +Form primary table: Person - * Processing of the resulting rows and columns: +Form slave table: Address - * In general, all columns of all rows will be printed out sequentially. +Relation: `Person.id = Address.personId` - On a per column base, printing of columns can be suppressed. This might be useful to select values which will be - accessed later on in another query via the {{level.columnname}} variable. To suppress printing of a column, use a - underscore as column name prefix. +* Form: wizard - Reserved column names have a special meaning and will be processed in a special way. See `Processing of columns in the SQL result`_ for details. + * Name: wizard + * Title: Person Wizard + * Table: Person + * Render: bootstrap - There are extensive ways to wrap columns and rows automatically. See :ref:`wrapping-rows-and-columns` +* *FormElement*: firstname + + * Class: **native** + * Type: **text** + * Name: firstname + * Label: Firstname + +* *FormElement*: email, text, 20 + + * Class: **native** + * Type: **text** + * Name: city + * Label: City + * Value: `{{SELECT city FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}` + +* *FormElement*: insert/update address record + + * Class: **action** + * Type: **afterSave** + * Label: Manage Address + * Parameter: + + * `slaveId={{SELECT id FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}` + * `sqlInsert={{INSERT INTO Address (personId, city) VALUES ({{r}}, '{{city:F:allbut:s}}') }}` + * `sqlUpdate={{UPDATE Address SET city='{{city:F:allbut:s}}' WHERE id={{slaveId:V}} }}` + * `sqlDelete={{DELETE FROM Address WHERE id={{slaveId:V}} AND ''='{{city:F:allbut:s}}' LIMIT 1}}` + +Form: Person Wizard - firstname, single note +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Requirement: A form that displays the column 'firstname' from table 'Person' and 'note' from table 'Note'. +If the records don't exist, the form should create it. +Column Person.noteId points to Note.id + +Form primary table: Person + +Form slave table: Address + +Relation: `Person.id = Address.personId` + +* Form: wizard + + * Name: wizard + * Title: Person Wizard + * Table: Person + * Render: bootstrap + +* *FormElement*: firstname + + * Class: **native** + * Type: **text** + * Name: firstname + * Label: Firstname + +* *FormElement*: email, text, 20 + + * Class: **native** + * Type: **text** + * Name: note + * Label: Note + * Value: `{{SELECT Note FROM Note AS n, Person AS p WHERE p.id={{r}} AND p.noteId=n.id ORDER BY id }}` + +* *FormElement*: insert/update address record + + * Class: **action** + * Type: **afterSave** + * Name: noteId + * Label: Manage Note + * Parameter: + + * `sqlInsert={{INSERT INTO Note (note) VALUES ('{{note:F:allbut:s}}') }}` + * `sqlUpdate={{UPDATE Note SET note='{{note:F:allbut:s}}' WHERE id={{slaveId:V}} }}` + +Icons Template Group +^^^^^^^^^^^^^^^^^^^^ + + * FormElement.parameter + +:: + + tgAddClass=btn alert-success + tgAddText=<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> + tgRemoveClass=btn btn-danger alert-danger + tgRemoveText=<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> + +Chart +^^^^^ + +* QFQ delivers a chart JavaScript lib: https://github.com/nnnick/Chart.js.git. Docs: http://www.chartjs.org/docs/ +* The library is not sourced in the HTML page automatically. To do it, either include the lib + `typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js`: + + * in the specific tt_content record (shown below in the example) or + * system wide via Typo3 Template record. + +* By splitting HTML and JavaScript code over several lines, take care not accidently to create a 'nesting'-end token. + Check the line after `10.tail =`. It's '}' alone on one line. This is a valid 'nesting'-end token!. There are two options + to circumvent this: + + * Don't nest the HTML & JavaScript code - bad workaround, this is not human readable. + * Select different nesting token, e.g. '<' / '>' (check the first line on the following example). + +:: + + # < + + 10.sql = SELECT '_' + 10.head = + <div style="height: 1024px; width: 640px;"> + <h3>Distribution of FormElement types over all forms</h3> + <canvas id="barchart" width="1240" height="640"></canvas> + </div> + <script src="typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js"></script> + <script> + $(function () { + var ctx = document.getElementById("barchart"); + var barChart = new Chart(ctx, { + type: 'bar', + data: { + + 10.tail = + } + }); + }); + </script> + + # Labels + 10.10 < + sql = SELECT "'", fe.type, "'" FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type + head = labels: [ + tail = ], + rsep = , + > + + # Data + 10.20 < + sql = SELECT COUNT(fe.id) FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type + head = datasets: [ { data: [ + tail = ], backgroundColor: "steelblue", label: "FormElements" } ] + rsep = , + > + +Upload Form Simple +^^^^^^^^^^^^^^^^^^ + +Table Person + ++---------------------+--------------+ +| Name | Type | ++=====================+==============+ +| id | int | ++---------------------+--------------+ +| name | varchar(255) | ++---------------------+--------------+ +| pathFileNamePicture | varchar(255) | ++---------------------+--------------+ +| pathFileNameAvatar | varchar(255) | ++---------------------+--------------+ + +* Form: + + * Name: UploadSimple + * Table: Person + +* FormElements: + + * Name: name + + * Type: text + * Label: Name + + * Name: pathFileNamePicture + + * Type: upload + * Label: Picture + * Parameter:: + + fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}} + + * Name: pathFileNameAvatar + + * Type: upload + * Label: Avatar + * Parameter:: + + fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}} + + +Upload Form Advanced 1 +^^^^^^^^^^^^^^^^^^^^^^ + +Table: Person + + +---------------------+--------------+ + | Name | Type | + +=====================+==============+ + | id | int | + +---------------------+--------------+ + | name | varchar(255) | + +---------------------+--------------+ + +Table: Note + + +---------------------+--------------+ + | Name | Type | + +=====================+==============+ + | id | int | + +---------------------+--------------+ + | pId | int | + +---------------------+--------------+ + | type | varchar(255) | + +---------------------+--------------+ + | pathFileName | varchar(255) | + +---------------------+--------------+ + +* Form: + + * Name: UploadAdvanced1 + * Table: Person + +* FormElements + + * Name: name + + * Type: text + * Label: Name + + * Name: mypathFileNamePicture + + * Type: upload + * Label: Picture + * Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }} + * Parameter:: + + fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}} + slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='picture' LIMIT 1}} + sqlInsert={{INSERT INTO Note (pathFileName, type, pId) VALUE ('{{fileDestination}}', 'picture', {{id:R0}}) }} + sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}} + sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} + + * Name: mypathFileNameAvatar + + * Type: upload + * Label: Avatar + * Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }} + * Parameter:: + + fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}} + slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='avatar' LIMIT 1}} + sqlInsert={{INSERT INTO Note (pathFileName, type, pId) VALUE ('{{fileDestination}}', 'avatar', {{id:R0}}) }} + sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}} + sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} + +Upload Form Advanced 2 +^^^^^^^^^^^^^^^^^^^^^^ + +Table: Person + + +---------------------+--------------+ + | Name | Type | + +=====================+==============+ + | id | int | + +---------------------+--------------+ + | name | varchar(255) | + +---------------------+--------------+ + | noteIdPicture | int | + +---------------------+--------------+ + | noteIdAvatar | int | + +---------------------+--------------+ + +Table: Note + + +---------------------+--------------+ + | Name | Type | + +=====================+==============+ + | id | int | + +---------------------+--------------+ + | pathFileName | varchar(255) | + +---------------------+--------------+ + +* Form: + + * Name: UploadAdvanced2 + * Table: Person + +* FormElements + + * Name: name + + * Type: text + * Label: Name + + * Name: mypathFileNamePicture + + * Type: upload + * Label: Picture + * Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }} + * Parameter:: + + fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}} + slaveId={{SELECT id FROM Note WHERE id={{noteIdPicture}} LIMIT 1}} + sqlInsert={{INSERT INTO Note (pathFileName) VALUE ('{{fileDestination}}') }} + sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}} + sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} + sqlAfter={{UPDATE Person SET noteIdPicture={{slaveId}} WHERE id={{id:R0}} LIMIT 1 + + * Name: mypathFileNameAvatar + + * Type: upload + * Label: Avatar + * Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }} + * Parameter:: + + fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}} + slaveId={{SELECT id FROM Note WHERE id={{noteIdAvatar}} LIMIT 1}} + sqlInsert={{INSERT INTO Note (pathFileName) VALUE ('{{fileDestination}}') }} + sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}} + sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} + sqlAfter={{UPDATE Person SET noteIdAvatar={{slaveId}} WHERE id={{id:R0}} LIMIT 1 + +FAQ +--- + + * Q: A variable {{<var>}} is shown as empty string, but there should be a value. + + * A: The sanatize rule is violeted and therefore the value has been removed. Set {{<var>:<store>:all}} as a test. + Only STORE_CLIENT and STORE_FORM will be sanatized. + + +Report +====== + +The QFQ extension is activated through tt-content records. One or more tt-content records per page are necessary to render +*forms* and *reports*. + +QFQ content element +------------------- + +QFQ is used by configuring Typo3 content elements. Insert one or more QFQ content elements on a Typo3 page. +Specify column and language per content record as wished. + +The title of the QFQ content element will not be rendered. It's only visible in the backend for orientation. + +General +------- + +To display a report on any given TYPO3 page, create a content element of type 'QFQ Element' (plugin) on that page. + +A simple example +^^^^^^^^^^^^^^^^ + +Assume that the database has a table person with columns firstName and lastName. To create a simple list of all persons, we can do the following: +:: + + 10.sql = SELECT id AS pId, CONCAT(firstName, " ", lastName, " ") AS name FROM person + +10 Stands for a *root level* of the report (see section `Structure`_). 10.sql defines a SQL query for this specific level. When the query is executed it will return a result having one single column name containing first and last name +separated by a space character. + +The HTML output displayed on the page resulting from only this definition could look as follows: +:: + + John DoeJane MillerFrank Star + +.. + + + +I.e., QFQ will simply output the content of the SQL result row after row for each single level. + +However, we can modify (wrap) the output by setting the values of various keys for each level: 10.rsep=<br/> for example tells QFQ to seperate the rows of the result by a HTML-line break. The final result in this case is: + +:: + + 10.sql = SELECT id AS personId, CONCAT(firstName, " ", lastName, " ") AS name FROM person + 10.sep = <br> + +HTML output: +:: + + John Doe<br>Jane Miller<br>Frank Star + +.. + +Syntax +------ + + All **root level queries** will be fired in the order specified by 'level' (Integer value). + + For **each** row of a query (this means *all* queries), all subqueries will be fired once. + + * E.g. if the outer query selects 5 rows, and a nested query always select 3 rows, than the total number of rows are 5 x 3 = 15 rows. + + There is a set of **variables** that will get replaced before the SQL-Query gets executed: + + Column values of the recent rows: {{<level>.<columnname>}} + + Global variables: {{global.<name>}} + + Variables from specific stores: {{<name>[:<store/s>[:<sanitize class>]]}} + + Current row index: {{<level>.line.count}} + + Total rows (num_rows for SELECT and SHOW, affected_rows for UPDATE and INSERT): {{<level>.line.total}} + + Last insert id for INSERT: {{<level>.line.insertId}} + + See :ref:`variables` for a full list of all available variables. + + Be aware that line.count / line.total have to be known before the query is fired. E.g. `10.sql = SELECT {{10.line.count}}, ... WHERE {{10.line.count}} = ...` + won't work as expected. `{{10.line.count}}` can't be replaced before the query is fired, but will be replaced during processing the result! + + + Different types of SQL queries are possible: SELECT, INSERT, UPDATE, DELETE, SHOW + + Only SELECT and SHOW queries will fire subqueries. + + * Processing of the resulting rows and columns: + + * In general, all columns of all rows will be printed out sequentially. + + On a per column base, printing of columns can be suppressed. This might be useful to select values which will be + accessed later on in another query via the {{level.columnname}} variable. To suppress printing of a column, use a + underscore as column name prefix. + + Reserved column names have a special meaning and will be processed in a special way. See `Processing of columns in the SQL result`_ for details. + + There are extensive ways to wrap columns and rows automatically. See :ref:`wrapping-rows-and-columns` Debug the bodytext ------------------ @@ -1830,10 +2584,9 @@ This would result in :: - - Marc Muster (3004 Bern) - Elton John (8008 Zürich) - Speedy Conzales (3012 Bern) + John Doe (3004 Bern) + Jane Miller (8008 Zürich) + Frank Star (3012 Bern) .. @@ -2988,502 +3741,303 @@ The same as above, but with braces:: rend = </li> } -Two queries: :: - - 10.sql = SELECT p.name FROM exp_person AS p - 10.rend = <br /> - 20.sql = SELECT a.street FROM exp_address AS a - 20.rend = <br /> - -Two queries: nested :: - - # outer query - 10.sql = SELECT p.name FROM exp_person AS p - 10.rend = <br /> - - # inner query - 10.10.sql = SELECT a.street FROM exp_address AS a - 10.10.rend = <br /> - -* For every record of '10', all records of 10.10 will be printed. - -Two queries: nested with variables :: - - # outer query - 10.sql = SELECT p.id, p.name FROM exp_person AS p - 10.rend = <br /> - - # inner query - 10.10.sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{10.id}}' - 10.10.rend = <br /> - -* For every record of '10', all assigned records of 10.10 will be printed. - -Two queries: nested with hidden variables in a table :: - - 10.sql = SELECT p.id AS _pId, p.name FROM exp_person AS p - 10.rend = <br /> - - # inner query - 10.10.sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{10.pId}}' - 10.10.rend = <br /> - -Same as above, but written in the nested notation :: - - 10 { - sql = SELECT p.id AS _pId, p.name FROM exp_person AS p - rend = <br /> - - 10 { - # inner query - sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{10.pId}}' - rend = <br /> - } - } - -* Columns starting with a '_' won't be printed but can be accessed as regular columns. - - -Best practice: Form -=================== - -Debug Report ------------- - -Writing "report's" in the nested notation or long queries broken over several lines, might not interpreted as wished. -Best for debugging is to specify in the tt-content record:: - - debugShowBodyText = 1 - -Note: Debug information is only display if it's enabled in *config.ini* by - * *SHOW_DEBUG_INFO=yes* or - * *SHOW_DEBUG_INFO=auto* and logged in in the same Browser as a Typo3 backend user. - -More detailed error messages ----------------------------- - -If *SHOW_DEBUG_INFO* is enabled, a full stacktrace and variable contents are displayed in case of an error. - -Person search form ------------------- - -QFQ content record:: - - # Creates a small form that redirects back to this page - 10 { - sql = SELECT '_' - head = <form action='#' method='get'><input type='hidden' name='id' value='{{pageId:T}}'>Search: <input type='text' name='search' value='{{search:CE:all}}'><input type='submit' value='Submit'></form> - } - - # SQL statement will find and list all the relevant forms - 20 { - sql = SELECT CONCAT('?detail&form=form&r=', f.id) AS _Pagee, f.id, f.name, f.title - FROM Form AS f - WHERE f.name LIKE '%{{search:CE:all}}%' - head = <table class='table'> - tail = </table> - rbeg = <tr> - rend = </tr> - fbeg = <td> - fend = </td> - } - - -Form: compute next free 'ord' automatically -------------------------------------------- - -Requirement: new records should automatically get the highest number plus 10 for their 'ord' value. Existing records -should not be altered. - -Version 1 -^^^^^^^^^ - -Compute the next 'ord' in advance in the subrecord field of the primary form. Submit that value to the new record -via SIP parameter to the secondary form. - -On the secondary form: for 'new' records choose the computed value, for existing records leave the value -unchanged. - -* Primary form, `subrecord` *FormElement*, field `parameter`: set :: - - detail=id:formId,{{SELECT '&', IFNULL(fe.ord,0)+10 FROM Form AS f LEFT JOIN *FormElement* AS fe ON fe.formId=f.id WHERE - f.id={{r:S0}} ORDER BY fe.ord DESC LIMIT 1}}:ord - - -* Secondary form, `ord` *FormElement*, field `value`: set - - :: - - `{{RS0}}`. - -Version 2 -^^^^^^^^^ - -Compute the next 'ord' as default value direct inside the secondary form. No change is needed for the primary form. - -* Secondary form, `ord` *FormElement*, field `value`: set `{{SELECT IF({{ord:R0}}=0, MAX(IFNULL(fe.ord,0))+10,{{ord:R0}}) FROM (SELECT 1) AS a LEFT JOIN FormElement AS fe ON fe.formId={{formId:S0}} GROUP BY fe.formId}}`. - -Form: Person Wizard - firstname, city -------------------------------------- - -Requirement: A form that displays the column 'firstname' from table 'Person' and 'city' from table 'Address'. If the -records not exist, the form should create it. - -Form primary table: Person - -Form slave table: Address - -Relation: `Person.id = Address.personId` - -* Form: wizard - - * Name: wizard - * Title: Person Wizard - * Table: Person - * Render: bootstrap - -* *FormElement*: firstname - - * Class: **native** - * Type: **text** - * Name: firstname - * Label: Firstname - -* *FormElement*: email, text, 20 - - * Class: **native** - * Type: **text** - * Name: city - * Label: City - * Value: `{{SELECT city FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}` - -* *FormElement*: insert/update address record - - * Class: **action** - * Type: **afterSave** - * Label: Manage Address - * Parameter: - - * `slaveId={{SELECT id FROM Address WHERE personId={{r}} ORDER BY id LIMIT 1}}` - * `sqlInsert={{INSERT INTO Address (personId, city) VALUES ({{r}}, '{{city:F:allbut:s}}') }}` - * `sqlUpdate={{UPDATE Address SET city='{{city:F:allbut:s}}' WHERE id={{slaveId:V}} }}` - * `sqlDelete={{DELETE FROM Address WHERE id={{slaveId:V}} AND ''='{{city:F:allbut:s}}' LIMIT 1}}` - -Form: Person Wizard - firstname, single note --------------------------------------------- - -Requirement: A form that displays the column 'firstname' from table 'Person' and 'note' from table 'Note'. -If the records don't exist, the form should create it. -Column Person.noteId points to Note.id - -Form primary table: Person - -Form slave table: Address - -Relation: `Person.id = Address.personId` - -* Form: wizard - - * Name: wizard - * Title: Person Wizard - * Table: Person - * Render: bootstrap - -* *FormElement*: firstname - - * Class: **native** - * Type: **text** - * Name: firstname - * Label: Firstname - -* *FormElement*: email, text, 20 - - * Class: **native** - * Type: **text** - * Name: note - * Label: Note - * Value: `{{SELECT Note FROM Note AS n, Person AS p WHERE p.id={{r}} AND p.noteId=n.id ORDER BY id }}` - -* *FormElement*: insert/update address record - - * Class: **action** - * Type: **afterSave** - * Name: noteId - * Label: Manage Note - * Parameter: +Two queries: :: - * `sqlInsert={{INSERT INTO Note (note) VALUES ('{{note:F:allbut:s}}') }}` - * `sqlUpdate={{UPDATE Note SET note='{{note:F:allbut:s}}' WHERE id={{slaveId:V}} }}` + 10.sql = SELECT p.name FROM exp_person AS p + 10.rend = <br /> + 20.sql = SELECT a.street FROM exp_address AS a + 20.rend = <br /> +Two queries: nested :: + # outer query + 10.sql = SELECT p.name FROM exp_person AS p + 10.rend = <br /> + # inner query + 10.10.sql = SELECT a.street FROM exp_address AS a + 10.10.rend = <br /> -Best Practise -============= +* For every record of '10', all records of 10.10 will be printed. -Central configured values -------------------------- +Two queries: nested with variables :: -Any variable in *config.qfq.ini* can be used by *{{<varname>:Y}}* in form or report statements. + # outer query + 10.sql = SELECT p.id, p.name FROM exp_person AS p + 10.rend = <br /> -E.g. + # inner query + 10.10.sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{10.id}}' + 10.10.rend = <br /> - TECHNICAL_CONTACT = jane.doe@example.net +* For every record of '10', all assigned records of 10.10 will be printed. -Could be used in an *FormElement.type* = sendmail with *parameter* setting *sendMailFrom={{TECHNICAL_CONTACT:Y}}*. +Two queries: nested with hidden variables in a table :: -Icons Template Group --------------------- + 10.sql = SELECT p.id AS _pId, p.name FROM exp_person AS p + 10.rend = <br /> - * FormElement.parameter + # inner query + 10.10.sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{10.pId}}' + 10.10.rend = <br /> -:: +Same as above, but written in the nested notation :: - tgAddClass=btn alert-success - tgAddText=<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> - tgRemoveClass=btn btn-danger alert-danger - tgRemoveText=<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> + 10 { + sql = SELECT p.id AS _pId, p.name FROM exp_person AS p + rend = <br /> + 10 { + # inner query + sql = SELECT a.street FROM exp_address AS a WHERE a.pId='{{10.pId}}' + rend = <br /> + } + } -Chart ------ +* Columns starting with a '_' won't be printed but can be accessed as regular columns. -* QFQ delivers a chart JavaScript lib: https://github.com/nnnick/Chart.js.git. Docs: http://www.chartjs.org/docs/ -* The library is not sourced in the HTML page automatically. To do it, either include the lib - `typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js`: - * in the specific tt_content record (shown below in the example) or - * system wide via Typo3 Template record. +.. _release: -* By splitting HTML and JavaScript code over several lines, take care not accidently to create a 'nesting'-end token. - Check the line after `10.tail =`. It's '}' alone on one line. This is a valid 'nesting'-end token!. There are two options - to circumvent this: +Release +======= - * Don't nest the HTML & JavaScript code - bad workaround, this is not human readable. - * Select different nesting token, e.g. '<' / '>' (check the first line on the following example). +Version 0.future +---------------- -:: +Changes +^^^^^^^ - # < + * Play formEditor.sql. - 10.sql = SELECT '_' - 10.head = - <div style="height: 1024px; width: 640px;"> - <h3>Distribution of FormElement types over all forms</h3> - <canvas id="barchart" width="1240" height="640"></canvas> - </div> - <script src="typo3conf/ext/qfq/Resources/Public/JavaScript/Chart.min.js"></script> - <script> - $(function () { - var ctx = document.getElementById("barchart"); - var barChart = new Chart(ctx, { - type: 'bar', - data: { + * Dropdownlist of containerassigment updated. - 10.tail = - } - }); - }); - </script> +Features +^^^^^^^^ - # Labels - 10.10 < - sql = SELECT "'", fe.type, "'" FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type - head = labels: [ - tail = ], - rsep = , - > +Bug Fixes +^^^^^^^^^ - # Data - 10.20 < - sql = SELECT COUNT(fe.id) FROM FormElement AS fe GROUP BY fe.type ORDER BY fe.type - head = datasets: [ { data: [ - tail = ], backgroundColor: "steelblue", label: "FormElements" } ] - rsep = , - > +Version 0.13 +------------ -Upload Form Simple ------------------- +Changes +^^^^^^^ -Table Person + * Play formEditor.sql. + * formEditor.sql: -+---------------------+--------------+ -| Name | Type | -+=====================+==============+ -| id | int | -+---------------------+--------------+ -| name | varchar(255) | -+---------------------+--------------+ -| pathFileNamePicture | varchar(255) | -+---------------------+--------------+ -| pathFileNameAvatar | varchar(255) | -+---------------------+--------------+ + * Checktype of `Form.name` restricted to `alnumx` (prior `all`). + * Changed `access` for Form `form` & '`ormElement` from `always` to `sip`. -* Form: + * Table `FormElement` - * Name: UploadSimple - * Table: Person + * Modified column: `checkType` - new value `numerical` -* FormElements: + ALTER TABLE FormElement MODIFY COLUMN checkType ENUM('alnumx','digit','numerical','email','min|max','min|max date', + 'pattern','allbut','all') NOT NULL DEFAULT 'alnumx' - * Name: name + * Example Report for `forms` extended by a delete button per row. - * Type: text - * Label: Name +Features +^^^^^^^^ - * Name: pathFileNamePicture + * print.php: offers 'print page' for any local page - create a PDF on the fly (printout is then browser independent). - * Type: upload - * Label: Picture - * Parameter:: + * Install `wkhtmltopdf` on the webserver (http://wkhtmltopdf.org/). + * In config.qfq.ini setup: - fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}} + BASE_URL_PRINT=http://www.../ + WKHTMLTOPDF=/opt/wkhtmltox/bin/wkhtmltopdf - * Name: pathFileNameAvatar + * Check and error report if 'php_intl' is missing. + * New Checktype 'allow numerical'. + * Documentation: example for 'radio' with no pre selection. + * #3063, Radios and checkboxes optional rendered in Bootstrap layout. + * Added 'help-box with-errors'-DIV after radios and checkboxes. + * Respect attribute `data-class-on-change` on save buttons. - * Type: upload - * Label: Avatar - * Parameter:: - fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}} +Bug Fixes +^^^^^^^^^ + * #2138 / digit sanitize: new class 'numerical' implemented. + * Fixed recursive thrown exception. + * #2064 / search of a default value for a non existing tablecolumn returns 'false'. -Upload Form Advanced 1 ----------------------- + * Fixed setting of STORE_SYSTEM / showDebugInfo during API call. -Table: Person + * #2081, #3180 Form: Label & note - update via `DynamicUpdate` + * #3253, if there is no STORE_TYPO3 (calls through .../api/ like save, delete, load): use SIP / CLIENT_TYPO3VARS. + * qfq-bs.css: - +---------------------+--------------+ - | Name | Type | - +=====================+==============+ - | id | int | - +---------------------+--------------+ - | name | varchar(255) | - +---------------------+--------------+ + * Alignment of checkboxes and radios optimized. + * CSS class 'qfq-note' for 'notes' (third column in a form). -Table: Note - +---------------------+--------------+ - | Name | Type | - +=====================+==============+ - | id | int | - +---------------------+--------------+ - | pId | int | - +---------------------+--------------+ - | type | varchar(255) | - +---------------------+--------------+ - | pathFileName | varchar(255) | - +---------------------+--------------+ +Version 0.12 +------------ -* Form: +Changes +^^^^^^^ - * Name: UploadAdvanced1 - * Table: Person + * Table 'FormElement' + * New column: rowLabelInputNote -* FormElements + ALTER TABLE `FormElement` ADD `rowLabelInputNote` set('row','label','/label','input','/input','note','/note','/row') + NOT NULL DEFAULT 'row,label,/label,input,/input,note,/note,/row' AFTER `bsNoteColumns` ; - * Name: name + * Modified column: 'type' - new value 'templateGroup' - * Type: text - * Label: Name + ALTER TABLE `FormElement` CHANGE `type` `type` ENUM( 'checkbox', 'date', 'datetime', 'dateJQW', 'datetimeJQW', 'extra', + 'gridJQW', 'text', 'editor', 'time', 'note', 'password', 'radio', 'select', 'subrecord', 'upload', 'fieldset', 'pill', + 'templateGroup', 'beforeLoad', 'beforeSave', 'beforeInsert', 'beforeUpdate', 'beforeDelete', 'afterLoad', 'afterSave', + 'afterInsert', 'afterUpdate', 'afterDelete', 'sendMail' ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'text'; - * Name: mypathFileNamePicture + * formEditor.sql: Added HTML 'placeholder' in FormEditor for bs*Columns. - * Type: upload - * Label: Picture - * Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }} - * Parameter:: + * PLAY 'formEditor.sql'. - fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}} - slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='picture' LIMIT 1}} - sqlInsert={{INSERT INTO Note (pathFileName, type, pId) VALUE ('{{fileDestination}}', 'picture', {{id:R0}}) }} - sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}} - sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} + * User Input will be UTF8 normalized. - * Name: mypathFileNameAvatar + * INSTALL 'php5-intl' or 'php7.0-intl' on Webserver. - * Type: upload - * Label: Avatar - * Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }} - * Parameter:: + * Add globalize.js to be included. Needed by jqx-all.js - fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}} - slaveId={{SELECT id FROM Note WHERE pId={{id:R0}} AND type='avatar' LIMIT 1}} - sqlInsert={{INSERT INTO Note (pathFileName, type, pId) VALUE ('{{fileDestination}}', 'avatar', {{id:R0}}) }} - sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}} - sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} + * UPDATE EXISTING TypoScript TEMPLATES of QFQ Installation. -Upload Form Advanced 2 ----------------------- + * Name of variable '_filename' (used in field 'parameter') has changed. Old: '_filename', New: 'filename' -Table: Person + * UPDATE `FormElement` SET parameter = REPLACE(parameter, '_filename', 'filename') - +---------------------+--------------+ - | Name | Type | - +=====================+==============+ - | id | int | - +---------------------+--------------+ - | name | varchar(255) | - +---------------------+--------------+ - | noteIdPicture | int | - +---------------------+--------------+ - | noteIdAvatar | int | - +---------------------+--------------+ -Table: Note +Features +^^^^^^^^ - +---------------------+--------------+ - | Name | Type | - +=====================+==============+ - | id | int | - +---------------------+--------------+ - | pathFileName | varchar(255) | - +---------------------+--------------+ + * User input will be UTF8 normalized + * config.qfq-ini: + * New configuration values: FORM_BS_LABEL_COLUMNS / FORM_BS_INPUT_COLUMNS / FORM_BS_NOTE_COLUMNS + * Comment empty variables - the new default setting is, that empty parameter in config.qfq.ini means EMPTY (=parameter is set and will not be overwritten by internal default), not UNDEFINED (overwritten by internal default). + * FileUpload: + * Implemented new Formelement.parameter: fileReplace=always - will replace existing files. + * Multiple / Advanced Upload: new logic implements slaveId, sqlInsert, sqlUpdate, sqlDelete. + * FormElement.parameter: sqlBefore / sqlAfter fired during 'Form' save for action elements. + * STORE FORM: variable 'filename' moved to STORE VAR - sanatize class needs no longer specified. + * STORE VAR: two new variables 'filename' and 'fileDestination' valid during processing of current upload FormElement. + * Default store priority list changed. Old: 'FSRD', New: 'FSRVD'. + * CODING.md: update doc for FormElement 'upload' and general 'Form' rendering & save (recursive rendering). + * User manual: + * Described form layout options: description for bsLabelColumn, bsInputColumn, bsNoteColumn + * Update 'file-upload' doc. + * Described 3 examples for upload forms. + * Administrator manual: + * Add description page.meta... + * New FormElement (type= 'container') added: 'templateGroup' + * FormElement.parameter.tgAddClass | tgAddText | tgRemoveClass | tgRemoveText | tgClass + * FormElement.maxSize: max number of duplicates + * #3230 templateGroup: margin between copies. 'tgClass' implemented. + * Native FormElements: + * FormElement.parameter.htlmlBefore|htmlAfter - add the specified HTML code before or after the element (outside of any wrapping) + * #3224, #3231 Html Tag <hr> als FormElement. >> htmlBefore | htmlAfter. + * FormElement.parameter.wrapLabel | wrapInput | wrapAfter | wrapRow - if specified, any default wrapping is omitted. + * FormElement.bsNoteColumns | bsInputColumns | bsNoteColumns - a '0' will suppress the whole rendering of the item. + * FormElement.rowLabelInputNote - switch on/off rendering of the corresponding system wrapping items. + * #3232 Define custom 'on-change' color - used for the save button: Form.parameter.buttonOnChangeClass=... + * Form.parameter & FormElement.parameter: Lines starting with '#' are treated as comments and will not be parsed. + +Bug fixes +^^^^^^^^^ -* Form: + * User manual: + * Fixed double include of validator.js in T3 Typoscript template example. + * Fixed wrong store name SYSTEM: S > Y + * Fixed wrong STORE_FORM variable names. + * Reformat FormElement.parameter description. + * Styling errors fixed. + * Use of 'decryptCurlyBraces()' to get better error messages. + * Skip unwanted parameter expansion during save. + * Fixed bug with uninitialized FE_SLAVE_ID + * formEditor.sql: + * The defintion as 'editor' (not text) for FormElement 'note' has been lost - reinserted. + * Fixed problem while playing SQL query - deleting old FormElements of Formeditor deleted also FormElements of other forms. + * #3066 / help-text with-error - CSS class 'hidden' will be rendered by default (as long there is no error). + * Labels are skipped, if FormElement.bsLabelColumns=0. + * Respect attribute `data-class-on-change` on save buttons. + +Version 0.11 +------------ - * Name: UploadAdvanced2 - * Table: Person +Features +^^^^^^^^ -* FormElements + * Added STORE_BEFORE, #3146 - Mainly used to compare old and new values during a form 'save' action. + * Added 'best practice' for defining and using of 'Central configure values' in UserManual. + * Added accent characters to sanatize class 'alnumx', #3183. + * Set default all QFQ send mails to 'auto-submit'. + * Added possibility to customize error messages ('data-pattern-error', 'data-rquired-error', 'data-match-error', + 'data-error') if validation fails. Customization can be done on global level (config.qfq.ini), per Form or per FormElement. + * *FormElement*: Double an input element and validate that the input match: FormElement.parameter.retype=1 + * Autofocus in Forms is now supported. By default the first Input Element receives the focus. Can be customized. + * Added a timestamp in shown exceptions. Usefull for screenshots, send by customer, to find the problem in SQL logfiles. + +Bug fixes +^^^^^^^^^ - * Name: name + * Fixed missing docutmentation for FormElement 'note'. + * Failed SQL queries will now always be logged, even if they do not modify some data. - * Type: text - * Label: Name +Version 0.10 +------------ - * Name: mypathFileNamePicture +Features +^^^^^^^^ - * Type: upload - * Label: Picture - * Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }} - * Parameter:: + * Implemented Parameter 'extraDeleteForm' for 'forms' and 'subrecords'. Update doc. - fileDestination=fileadmin/user/{{id:R0}}-picture-{{filename}} - slaveId={{SELECT id FROM Note WHERE id={{noteIdPicture}} LIMIT 1}} - sqlInsert={{INSERT INTO Note (pathFileName) VALUE ('{{fileDestination}}') }} - sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}} - sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} - sqlAfter={{UPDATE Person SET noteIdPicture={{slaveId}} WHERE id={{id:R0}} LIMIT 1 +Bug fixes +^^^^^^^^^ - * Name: mypathFileNameAvatar + * Suppress rendering of form title during a 'delete' call. No one will see it and required parameters are not supplied. + * In case of broken SQL queries, print them in ajax error message. + * Remove parameter 'table' from Delete SIP URLs. ToolTip updated. - * Type: upload - * Label: Avatar - * Value: {{SELECT pathFileName FROM Note WHERE id={{slaveId}} }} - * Parameter:: +Version 0.9 +----------- - fileDestination=fileadmin/user/{{id:R0}}-avatar-{{filename}} - slaveId={{SELECT id FROM Note WHERE id={{noteIdAvatar}} LIMIT 1}} - sqlInsert={{INSERT INTO Note (pathFileName) VALUE ('{{fileDestination}}') }} - sqlUpdate={{UPDATE Note SET pathFileName = '{{fileDestination}}' WHERE id={{slaveId}} LIMIT 1}} - sqlDelete={{DELETE FROM Note WHERE id={{slaveId}} LIMIT 1}} - sqlAfter={{UPDATE Person SET noteIdAvatar={{slaveId}} WHERE id={{id:R0}} LIMIT 1 +Features +^^^^^^^^ -FAQ -=== + * FormEditor: + * design update - new default background color: grey. + * per form configureable background colors. + * Optional right align of all form element labels. + * Added config.qfq.ini values CSS_CLASS_QFQ_FORM_PILL, CSS_CLASS_QFQ_FORM_BODY, CSS_CLASS_QFQ_CONTAINER. - * Q: A variable {{<var>}} is shown as empty string, but there should be a value. +Bug fixes +^^^^^^^^^ - * A: The sanatize rule is violeted and therefore the value has been removed. Set {{<var>:<store>:all}} as a test. - Only STORE_CLIENT and STORE_FORM will be sanatized. + * BuildFormBootstrap.php: added new class name 'qfq-label' to form labels - needed to assign 'qfq-form-right' class. Changed wrapping of formelements from 'col-md-8' (wrong) to 'col-md-12'. + * QuickFormQuery.php: Set default for new F_CLASS_PILL & F_CLASS_BODY. + * formEditor.sql: New default background color for formElements is blue. + * qfq-bs.css.less: add classes qfq-form-pill, qfq-form-body, form-group (center), qfq-color-..., qfq-form-right. + * Index.rst: Add note to hierachy chars. Fixed uncomplete doc to a) bs*Columns, showButton. Add classPill, classBody. Rewrote form.paramter.class. + * QuickFormQuery.php: Button save/ close/ delete/ new - align to right border of form. + * UsersManual/index.rst: renamed chapter for formelements. Cleanup formelement types. Wrote chapter 'Detailed concept'. + * QuickFormQuery.php, FormAction.php: '#2931 / afterSave Hauptrecord xId nicht direkt verfügbar' - load master record again, after 'action'-elements has been processed. + * UsersManual/index.rst: Startet FAQ section. + * config.qfq.example.ini: Added comment where to save config.qfq.ini. + * UsersManual/index.rst: Rewrite of 'action'-FormElement definition. + * #2739: beforeDelete / afterDelete. + * PROTOCOL.md: update 'delete' description. + * delete.php: fixed unwanted loose of MSG_CONTENT. + * Report.php: Fixed double '&&' in building UrlParam. + * FormAction.php: In case of 'AFTER_DELETE', do not try to load primary record - that one is already deleted. + * Sip.php: Do not skip SIP_TARGET_URL as parameter for the SIP. + * #3001 Report: delete implementieren. + * Index.rst, Constants.php: reverted parameter '_table' in delete links back to 'table' - Reason: 'form' needs to be 'form' (instead of '_form') due to many used places already. + * Sip.php: move SIP_TARGET_URL back to stored inside SIP - it's necessary for 'delete'-links. + * Report.php, Constants.php: Remove code to handle unecessary 'p:' tag for delete links. + * Link.php: Check paged / Paged that the parameter r, table and form are given in the right combination. + * Link.php, Report.php: New '_link' token 'x'. '_paged' and '_Paged' are rendered via Link() class, Link() class now supports delete links. + * QuickFormQuery.php: for modeForm='Form Delete' the 'required param' are not respected - this makes sense, cause these parameters typically filled in newly created records. + * Fixed: #3076 Delete Button bei Subrecords erzeugt sporadisch Javascript Exceptions (Webkit: Chrome / Vivaldi) - kein loeschen moeglich. diff --git a/extension/Documentation/Settings.yml b/extension/Documentation/Settings.yml index e53326df985a320c8c211199f0da5f6f97f44356..2003550853af0946e68c13e7280e109e18f1e636 100644 --- a/extension/Documentation/Settings.yml +++ b/extension/Documentation/Settings.yml @@ -6,8 +6,8 @@ conf.py: copyright: 2017 project: QFQ Extension - version: 0.12.0 - release: 0.12.0 + version: 0.13.0 + release: 0.13.0 latex_documents: - - Index - qfq.tex diff --git a/extension/Documentation/UsersManual/Index.rst.orig b/extension/Documentation/UsersManual/Index.rst.orig deleted file mode 100644 index de33ebd7417fff189be025bca5bbdb968d06c792..0000000000000000000000000000000000000000 --- a/extension/Documentation/UsersManual/Index.rst.orig +++ /dev/null @@ -1,49 +0,0 @@ -.. ================================================== -.. FOR YOUR INFORMATION -.. -------------------------------------------------- -.. -*- coding: utf-8 -*- with BOM. - -.. include:: ../Includes.txt - - -.. _users-manual: - -Users manual -============ - -Documentation of how to use the extension, how it works, how to apply it, if it's a website plugin. - -Language should be non-technical, explaining, using small examples. Don't use to many acronyms unless they have been explained. - -Examples: For the "News" plugin this would be a manual showing how to create the news items, explaining the options etc. - -Provide screenshots of a neutral Backend such as the Backend of the Introduction Package for instance. Have in mind that the User manual could possibly be re-used in a larger documentation compilation, for example when a company generates a documentation for its client. - -Target group: **Users** - -.. figure:: ../Images/UserManual/BackendView.png - :width: 500px - :alt: Backend view - - Default Backend view (caption of the image) - - The Backend view of TYPO3 after the user has clicked on module "Page". (legend of the image) - - -Link to official documentation ------------------------------- - -Sphinx makes it easy to link to official TYPO3 documentation: - -- :ref:`TYPO3 Tutorial for Editors <t3editors:start>` -- :ref:`Getting Started Tutorial <t3start:start>` - -and you may even link to a very specific chapter explaining how to :ref:`create a browser condition <t3tsref:condition-browser>` within the TypoScript Reference. - -For a complete reference of available cross-link prefixes, please consult file ``_make/conf.py``. - - -FAQ -^^^ - -Possible subsection: FAQ diff --git a/extension/Documentation/_make/conf.py b/extension/Documentation/_make/conf.py index f967a105c6129c9d230076aaa2048eb494dceca3..6d448ae948cef00d3f1ef13b5370b88a11cfb042 100644 --- a/extension/Documentation/_make/conf.py +++ b/extension/Documentation/_make/conf.py @@ -57,9 +57,9 @@ copyright = u'2017, Carsten Rose' # built documents. # # The short X.Y version. -version = '0.12' +version = '0.13' # The full version, including alpha/beta/rc tags. -release = '0.12.0' +release = '0.13.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/extension/Documentation/Introduction/Index.rst b/extension/Documentation/unused_Introduction/Index.rst similarity index 100% rename from extension/Documentation/Introduction/Index.rst rename to extension/Documentation/unused_Introduction/Index.rst diff --git a/extension/RELEASE.txt b/extension/RELEASE.txt index 5e252a995fd1ce3b6ab21ed9bcc9182a4c573cd6..4127f5d0ae7a901ae0e247b1a026fc95fec616da 100644 --- a/extension/RELEASE.txt +++ b/extension/RELEASE.txt @@ -1,37 +1,68 @@ +Release +======= + Version 0.13 -============ +------------ Changes -------- +^^^^^^^ - * Table 'FormElement' + * Play formEditor.sql. + * formEditor.sql: + + * Checktype of `Form.name` restricted to `alnumx` (prior `all`). + * Changed `access` for Form `form` & '`ormElement` from `always` to `sip`. + + * Table `FormElement` - * Modified column: 'checkType' - new value 'numerical' + * Modified column: `checkType` - new value `numerical` ALTER TABLE FormElement MODIFY COLUMN checkType ENUM('alnumx','digit','numerical','email','min|max','min|max date', 'pattern','allbut','all') NOT NULL DEFAULT 'alnumx' + * Example Report for `forms` extended by a delete button per row. Features --------- +^^^^^^^^ - * print.php + * print.php: offers 'print page' for any local page - create a PDF on the fly (printout is then browser independent). * Install `wkhtmltopdf` on the webserver (http://wkhtmltopdf.org/). - * In config.qfq.ini setup BASE_URL_PRINT, WKHTMLTOPDF. + * In config.qfq.ini setup: + + BASE_URL_PRINT=http://www.../ + WKHTMLTOPDF=/opt/wkhtmltox/bin/wkhtmltopdf + + * Check and error report if 'php_intl' is missing. + * New Checktype 'allow numerical'. + * Documentation: example for 'radio' with no pre selection. + * #3063, Radios and checkboxes optional rendered in Bootstrap layout. + * Added 'help-box with-errors'-DIV after radios and checkboxes. + * Respect attribute `data-class-on-change` on save buttons. Bug Fixes ---------- +^^^^^^^^^ * #2138 / digit sanitize: new class 'numerical' implemented. + * Fixed recursive thrown exception. + * #2064 / search of a default value for a non existing tablecolumn returns 'false'. + + * Fixed setting of STORE_SYSTEM / showDebugInfo during API call. + + * #2081, #3180 Form: Label & note - update via `DynamicUpdate` + * #3253, if there is no STORE_TYPO3 (calls through .../api/ like save, delete, load): use SIP / CLIENT_TYPO3VARS. + * qfq-bs.css: + + * Alignment of checkboxes and radios optimized. + * CSS class 'qfq-note' for 'notes' (third column in a form). Version 0.12 -============ +------------ Changes -------- +^^^^^^^ * Table 'FormElement' * New column: rowLabelInputNote @@ -58,23 +89,26 @@ Changes * UPDATE EXISTING TypoScript TEMPLATES of QFQ Installation. + * Variable field parameter has changed. old '_filename', new 'filename' + + * UPDATE `FormElement` SET parameter = REPLACE(parameter, '_filename', 'filename') Features --------- +^^^^^^^^ * User input will be UTF8 normalized - * config.qfq-ini: + * config.qfq-ini: * New configuration values: FORM_BS_LABEL_COLUMNS / FORM_BS_INPUT_COLUMNS / FORM_BS_NOTE_COLUMNS * Comment empty variables - the new default setting is, that empty parameter in config.qfq.ini means EMPTY (=parameter is set and will not be overwritten by internal default), not UNDEFINED (overwritten by internal default). - * FileUpload: + * FileUpload: * Implemented new Formelement.parameter: fileReplace=always - will replace existing files. * Multiple / Advanced Upload: new logic implements slaveId, sqlInsert, sqlUpdate, sqlDelete. * FormElement.parameter: sqlBefore / sqlAfter fired during 'Form' save for action elements. * STORE FORM: variable 'filename' moved to STORE VAR - sanatize class needs no longer specified. * STORE VAR: two new variables 'filename' and 'fileDestination' valid during processing of current upload FormElement. - * Default store priority list changed. Old: 'FSRD', New: 'FSRVD'. + * Default store priority list changed. Old: 'FSRD', New: 'FSRVD'. * CODING.md: update doc for FormElement 'upload' and general 'Form' rendering & save (recursive rendering). - * User manual: + * User manual: * Described form layout options: description for bsLabelColumn, bsInputColumn, bsNoteColumn * Update 'file-upload' doc. * Described 3 examples for upload forms. @@ -94,9 +128,9 @@ Features * Form.parameter & FormElement.parameter: Lines starting with '#' are treated as comments and will not be parsed. Bug fixes ---------- +^^^^^^^^^ - * User manual: + * User manual: * Fixed double include of validator.js in T3 Typoscript template example. * Fixed wrong store name SYSTEM: S > Y * Fixed wrong STORE_FORM variable names. @@ -105,7 +139,7 @@ Bug fixes * Use of 'decryptCurlyBraces()' to get better error messages. * Skip unwanted parameter expansion during save. * Fixed bug with uninitialized FE_SLAVE_ID - * formEditor.sql: + * formEditor.sql: * The defintion as 'editor' (not text) for FormElement 'note' has been lost - reinserted. * Fixed problem while playing SQL query - deleting old FormElements of Formeditor deleted also FormElements of other forms. * #3066 / help-text with-error - CSS class 'hidden' will be rendered by default (as long there is no error). @@ -113,10 +147,11 @@ Bug fixes * Respect attribute `data-class-on-change` on save buttons. Version 0.11 -============ +------------ Features --------- +^^^^^^^^ + * Added STORE_BEFORE, #3146 - Mainly used to compare old and new values during a form 'save' action. * Added 'best practice' for defining and using of 'Central configure values' in UserManual. * Added accent characters to sanatize class 'alnumx', #3183. @@ -128,30 +163,32 @@ Features * Added a timestamp in shown exceptions. Usefull for screenshots, send by customer, to find the problem in SQL logfiles. Bug fixes ---------- +^^^^^^^^^ + * Fixed missing docutmentation for FormElement 'note'. * Failed SQL queries will now always be logged, even if they do not modify some data. Version 0.10 -============ +------------ Features --------- +^^^^^^^^ * Implemented Parameter 'extraDeleteForm' for 'forms' and 'subrecords'. Update doc. Bug fixes ---------- +^^^^^^^^^ * Suppress rendering of form title during a 'delete' call. No one will see it and required parameters are not supplied. * In case of broken SQL queries, print them in ajax error message. * Remove parameter 'table' from Delete SIP URLs. ToolTip updated. Version 0.9 -=========== +----------- Features --------- +^^^^^^^^ + * FormEditor: * design update - new default background color: grey. * per form configureable background colors. @@ -159,12 +196,12 @@ Features * Added config.qfq.ini values CSS_CLASS_QFQ_FORM_PILL, CSS_CLASS_QFQ_FORM_BODY, CSS_CLASS_QFQ_CONTAINER. Bug fixes ---------- +^^^^^^^^^ * BuildFormBootstrap.php: added new class name 'qfq-label' to form labels - needed to assign 'qfq-form-right' class. Changed wrapping of formelements from 'col-md-8' (wrong) to 'col-md-12'. * QuickFormQuery.php: Set default for new F_CLASS_PILL & F_CLASS_BODY. * formEditor.sql: New default background color for formElements is blue. - * qfq-bs.css.less: add classes qfq-form-pill, qfq-form-body, form-group (center), qfq-color-*, qfq-form-right. + * qfq-bs.css.less: add classes qfq-form-pill, qfq-form-body, form-group (center), qfq-color-..., qfq-form-right. * Index.rst: Add note to hierachy chars. Fixed uncomplete doc to a) bs*Columns, showButton. Add classPill, classBody. Rewrote form.paramter.class. * QuickFormQuery.php: Button save/ close/ delete/ new - align to right border of form. * UsersManual/index.rst: renamed chapter for formelements. Cleanup formelement types. Wrote chapter 'Detailed concept'. diff --git a/extension/ext_emconf.php b/extension/ext_emconf.php index 3177bc344873dca670105571a0aae4131052a64a..4b0ed5634015d94b5f3d60d8a9878ee24e184155 100644 --- a/extension/ext_emconf.php +++ b/extension/ext_emconf.php @@ -10,5 +10,5 @@ $EM_CONF[$_EXTKEY] = array( 'dependencies' => 'fluid,extbase', 'clearcacheonload' => true, 'state' => 'alpha', - 'version' => '0.12.0' + 'version' => '0.13.0' ); \ No newline at end of file diff --git a/extension/qfq/api/load.php b/extension/qfq/api/load.php index fcc474ec8b725e574484d8f13a3a5679ca2a3f3d..f1ae905f5fe1119772237f20e0ae741788a32a75 100644 --- a/extension/qfq/api/load.php +++ b/extension/qfq/api/load.php @@ -65,8 +65,9 @@ try { $answer[API_STATUS] = API_ANSWER_STATUS_SUCCESS; $answer[API_MESSAGE] = 'load: success'; $answer[API_FORM_UPDATE] = $data; + $answer[API_ELEMENT_UPDATE] = $data[API_ELEMENT_UPDATE]; - unset($data[API_ELEMENT_UPDATE]); + unset($answer[API_FORM_UPDATE][API_ELEMENT_UPDATE]); } catch (qfq\UserFormException $e) { $answer[API_MESSAGE] = $e->formatMessage(); diff --git a/extension/qfq/api/typeahead.php b/extension/qfq/api/typeahead.php new file mode 100644 index 0000000000000000000000000000000000000000..e635c8ec782044f6b870db0d5ac54243064fe2a9 --- /dev/null +++ b/extension/qfq/api/typeahead.php @@ -0,0 +1,35 @@ +<?php +/** + * Created by PhpStorm. + * User: ep + * Date: 12/23/15 + * Time: 6:17 PM + */ + + +namespace qfq; + +use qfq; + +require_once(__DIR__ . '/../qfq/form/TypeAhead.php'); +require_once(__DIR__ . '/../qfq/Constants.php'); + + +/** + * Return JSON encoded answer + * + */ + + +try { + $qfq = new \qfq\TypeAhead(); + + $answer = $qfq->process(); + +} catch (\Exception $e) { + $answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage(); +} + +header("Content-Type: application/json"); +echo json_encode($answer); + diff --git a/extension/qfq/qfq/AbstractBuildForm.php b/extension/qfq/qfq/AbstractBuildForm.php index 686236080eb4ed116b55fc3b02673a81c95674bd..849b0f73fafde49df56a29b4903721c72f26be43 100644 --- a/extension/qfq/qfq/AbstractBuildForm.php +++ b/extension/qfq/qfq/AbstractBuildForm.php @@ -9,9 +9,10 @@ namespace qfq; use qfq; -use qfq\Store; -use qfq\OnArray; -use qfq\UserFormException; + +//use qfq\Store; +//use qfq\OnArray; +//use qfq\UserFormException; require_once(__DIR__ . '/../qfq/store/Store.php'); require_once(__DIR__ . '/../qfq/Constants.php'); @@ -725,10 +726,20 @@ abstract class AbstractBuildForm { public function buildInput(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) { $textarea = ''; $attribute = ''; + $class = 'form-control'; + + $typeAheadUrlParam = $this->typeAheadBuildParam($formElement); + if ($typeAheadUrlParam != '') { + $class .= ' ' . CLASS_TYPEAHEAD; + $dataSip = $this->sip->queryStringToSip($typeAheadUrlParam, RETURN_SIP); + $attribute .= Support::doAttribute(DATA_TYPEAHEAD_SIP, $dataSip); + $attribute .= Support::doAttribute(DATA_TYPEAHEAD_LIMIT, $formElement[FE_TYPEAHEAD_LIMIT]); + $attribute .= Support::doAttribute(DATA_TYPEAHEAD_MINLENGTH, $formElement[FE_TYPEAHEAD_MINLENGTH]); + } $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]); $attribute .= Support::doAttribute('name', $htmlFormElementName); - $attribute .= Support::doAttribute('class', 'form-control'); + $attribute .= Support::doAttribute('class', $class); if (isset($formElement[FE_RETYPE_SOURCE_NAME])) { $htmlFormElementNamePrimary = str_replace(RETYPE_FE_NAME_EXTENSION, '', $htmlFormElementName); @@ -774,6 +785,69 @@ abstract class AbstractBuildForm { } + /** + * Check $formElement for FE_TYPE_AHEAD_SQL or FE_TYPE_AHEAD_LDAP_SERVER. + * If one of them is given: fill $urlParam. + * Set some parameter for later outside use, especially FE_TYPEAHEAD_LIMIT, FE_TYPEAHEAD_MINLENGTH + * + * @param array $formElement + * @return string + */ + private function typeAheadBuildParam(array &$formElement) { + + $urlParam = ''; + + $formElement[FE_TYPEAHEAD_LIMIT] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LIMIT, TYPEAHEAD_DEFAULT_LIMIT); + $formElement[FE_TYPEAHEAD_MINLENGTH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_MINLENGTH, 2); + + if (isset($formElement[FE_TYPEAHEAD_SQL])) { + $sql = $this->checkSqlAppendLimit($formElement[FE_TYPEAHEAD_SQL], $formElement[FE_TYPEAHEAD_LIMIT]); + $urlParam = FE_TYPEAHEAD_SQL . '=' . $sql; + } elseif (isset($formElement[FE_TYPEAHEAD_LDAP_SERVER])) { + $formElement[FE_TYPEAHEAD_LDAP_SERVER] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SERVER); + $formElement[FE_TYPEAHEAD_LDAP_BASE_DN] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_BASE_DN); + $formElement[FE_TYPEAHEAD_LDAP_SEARCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SEARCH); + $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_VALUE_PRINTF); + $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_KEY_PRINTF); + + $arr = [ + FE_TYPEAHEAD_LDAP_SERVER => $formElement[FE_TYPEAHEAD_LDAP_SERVER], + FE_TYPEAHEAD_LDAP_BASE_DN => $formElement[FE_TYPEAHEAD_LDAP_BASE_DN], + FE_TYPEAHEAD_LDAP_SEARCH => $formElement[FE_TYPEAHEAD_LDAP_SEARCH], + FE_TYPEAHEAD_LDAP_VALUE_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF], + FE_TYPEAHEAD_LDAP_KEY_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF], + FE_TYPEAHEAD_LIMIT => $formElement[FE_TYPEAHEAD_LIMIT], + ]; + + $urlParam = OnArray::toString($arr); + } + + return $urlParam; + } + + /** + * Checks if $sql contains a SELECT statement. + * Check for existence of a LIMIT Parameter. If not found add one. + * + * @param $sql + * @param $limit + * @return string Checked and maybe extended $sql statement. + * @throws \qfq\UserFormException + */ + private function checkSqlAppendLimit($sql, $limit) { + $sql = trim($sql); + + if (false === stristr(substr($sql, 0, 7), 'SELECT ')) { + throw new UserFormException("Expect a SELECT statement in " . FE_TYPEAHEAD_SQL . " - got: " . $sql, ERROR_BROKEN_PARAMETER); + } + + if (false === stristr($sql, ' LIMIT ')) { + $sql .= " LIMIT $limit"; + } + + return $sql; + } + /** * Calculates the maxlength of an input field, based on formElement type, formElement user definition and table.field definition. * @@ -1062,9 +1136,6 @@ abstract class AbstractBuildForm { $itemKey = array(); $itemValue = array(); - if (count($formElement) < 20) - throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT); - // Call getItemsForEnumOrSet() only if there a corresponding column really exist. if (false !== $this->store->getVar($formElement['name'], STORE_TABLE_COLUMN_TYPES)) { $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType); diff --git a/extension/qfq/qfq/Constants.php b/extension/qfq/qfq/Constants.php index 727f712eacb8b833e7464ae119f29c5110c690ee..0fba784aa5251769243b4466f995c02b4f231554 100644 --- a/extension/qfq/qfq/Constants.php +++ b/extension/qfq/qfq/Constants.php @@ -134,7 +134,7 @@ const ERROR_LOG_NOT_WRITABLE = 1045; const ERROR_UNNOWN_STORE = 1046; const ERROR_GET_STORE_ZERO = 1047; const ERROR_SET_STORE_ZERO = 1048; -const ERROR_MISSING_FORMELEMENT = 1049; + const ERROR_INVALID_OR_MISSING_PARAMETER = 1050; const ERROR_UNKNOWN_SQL_LOG_MODE = 1051; const ERROR_FORM_NOT_FOUND = 1052; @@ -162,6 +162,7 @@ const ERROR_OVERWRITE_RECORD_ID = 1073; const ERROR_MISSING_SLAVE_ID_DEFINITION = 1074; const ERROR_MISSING_INTL = 1075; const ERROR_HTML_TOKEN_TOO_SHORT = 1076; +const ERROR_MISSING_PRINTF_ARGUMENTS = 1077; // Subrecord const ERROR_SUBRECORD_MISSING_COLUMN_ID = 1100; @@ -198,6 +199,8 @@ const ERROR_UPLOAD = 1500; const ERROR_UNKNOWN_ACTION = 1502; const ERROR_NO_TARGET_PATH_FILE_NAME = 1503; +const ERROR_LDAP_CONNECT = 1600; + // KeyValueParser const ERROR_KVP_VALUE_HAS_NO_KEY = 1900; @@ -373,7 +376,18 @@ const VAR_FILE_DESTINATION = 'fileDestination'; const VAR_SLAVE_ID = ACTION_KEYWORD_SLAVE_ID; const VAR_FILENAME = 'filename'; // Original filename of an uploaded file. -//const RECORD_ID_NEW = -1; + +// PHP class DB can operate in these modes +const MODE_DB_REGULAR = 'regular'; +const MODE_DB_NO_LOG = 'noLog'; + +// PHPO class Typeahead +const TYPEAHEAD_API_QUERY = 'query'; // Name of parameter in API call of typeahead.php?query=...&s=... - See also FE_TYPE_AHEAD_SQL +const TYPEAHEAD_API_SIP = 'sip'; // Name of parameter in API call of typeahead.php?query=...&s=... +const TYPEAHEAD_DEFAULT_LIMIT = 20; + +const SINGLE_TICK = "'"; +const DOUBLE_TICK = '"'; // TOKEN evaluate const TOKEN_ESCAPE_SINGLE_TICK = 's'; @@ -431,16 +445,27 @@ const API_JSON_HIDDEN = 'hidden'; const API_JSON_DISABLED = 'disabled'; const API_JSON_REQUIRED = 'required'; -const DATA_HIDDEN = 'data-hidden'; -const DATA_DISABLED = 'data-disabled'; -const DATA_REQUIRED = 'data-required'; - const API_ANSWER_STATUS_SUCCESS = 'success'; const API_ANSWER_STATUS_ERROR = 'error'; const API_ANSWER_REDIRECT_CLIENT = 'client'; const API_ANSWER_REDIRECT_NO = 'no'; const API_ANSWER_REDIRECT_URL = 'url'; +const API_TYPEAHEAD_KEY = 'key'; +const API_TYPEAHEAD_VALUE = 'value'; + +const DATA_HIDDEN = 'data-hidden'; +const DATA_DISABLED = 'data-disabled'; +const DATA_REQUIRED = 'data-required'; + +const CLASS_TYPEAHEAD = 'qfq-typeahead'; +const DATA_TYPEAHEAD_SIP = 'data-typeahead-sip'; // Used for typeAhead +//const CLASS_TYPEAHEAD = 'qfq-type-ahead'; +//const DATA_TYPEAHEAD_SIP = 'data-sip'; // Used for typeAhead + +const DATA_TYPEAHEAD_LIMIT = 'data-typeahead-limit'; +const DATA_TYPEAHEAD_MINLENGTH = 'data-typeahead-minlength'; + // BuildForm const SYMBOL_NEW = 'new'; const SYMBOL_EDIT = 'edit'; @@ -580,11 +605,18 @@ const FE_TEMPLATE_GROUP_CLASS = 'tgClass'; const FE_TEMPLATE_GROUP_DEFAULT_MAX_LENGTH = 5; const FE_TEMPLATE_GROUP_NAME_PATTERN = '%d'; const FE_BUTTON_CLASS = 'buttonClass'; +const FE_TYPEAHEAD_LIMIT = 'typeaheadLimit'; +const FE_TYPEAHEAD_MINLENGTH = 'typeaheadMinLength'; +const FE_TYPEAHEAD_SQL = 'typeAheadSql'; +const FE_TYPEAHEAD_LDAP_SERVER = 'typeAheadLdapServer'; +const FE_TYPEAHEAD_LDAP_BASE_DN = 'typeAheadLdapBaseDn'; +const FE_TYPEAHEAD_LDAP_SEARCH = 'typeAheadLdapSearch'; +const FE_TYPEAHEAD_LDAP_VALUE_PRINTF = 'typeAheadLdapValuePrintf'; +const FE_TYPEAHEAD_LDAP_KEY_PRINTF = 'typeAheadLdapKeyPrintf'; const RETYPE_FE_NAME_EXTENSION = 'RETYPE'; const FE_HTML_ID = 'htmlId'; // Will be dynamically computed during runtime. - // FormElement Types const FE_TYPE_UPLOAD = 'upload'; const FE_TYPE_EXTRA = 'extra'; @@ -699,4 +731,5 @@ const COLUMN_PAGEE = "pagee"; const COLUMN_PAGEH = "pageh"; const COLUMN_PAGEI = "pagei"; const COLUMN_PAGEN = "pagen"; -const COLUMN_PAGES = "pages"; \ No newline at end of file +const COLUMN_PAGES = "pages"; + diff --git a/extension/qfq/qfq/Database.php b/extension/qfq/qfq/Database.php index 18f23d48db3a8427c85a126b2ded199cd9b96aef..bda86fc36bce03a4722331067999e3e24be0e922 100644 --- a/extension/qfq/qfq/Database.php +++ b/extension/qfq/qfq/Database.php @@ -20,6 +20,7 @@ require_once(__DIR__ . '/exceptions/CodeException.php'); require_once(__DIR__ . '/exceptions/DbException.php'); require_once(__DIR__ . '/store/Store.php'); +require_once(__DIR__ . '/store/Config.php'); require_once(__DIR__ . '/helper/Support.php'); require_once(__DIR__ . '/helper/Logger.php'); require_once(__DIR__ . '/helper/BindParam.php'); @@ -62,16 +63,29 @@ class Database { * @throws CodeException * @throws UserFormException */ - public function __construct() { - $this->store = Store::getInstance(); + public function __construct($mode = MODE_DB_REGULAR) { + $dbInit = ''; + + switch ($mode) { + case MODE_DB_REGULAR: + $this->store = Store::getInstance(); + $config = $this->store->getStore(STORE_SYSTEM); + $this->sqlLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM); + $dbInit = $this->store->getVar(SYSTEM_DB_INIT, STORE_SYSTEM); + break; + case MODE_DB_NO_LOG: + $configClass = new Config(); + $config = $configClass->readConfig(); + break; + default: + throw new \qfq\CodeException('Unknown mode: ' . $mode, ERROR_UNKNOWN_MODE); + } if ($this->mysqli === null) { - $this->mysqli = $this->dbConnect(); + $this->mysqli = $this->dbConnect($config); } - $this->sqlLog = $this->store->getVar(SYSTEM_SQL_LOG, STORE_SYSTEM); // DB Init - $dbInit = $this->store->getVar(SYSTEM_DB_INIT, STORE_SYSTEM); if ($dbInit !== false && $dbInit != '') { $this->sql($dbInit); } @@ -84,18 +98,13 @@ class Database { * @return \mysqli * @throws UserFormException */ - private function dbConnect() { + private function dbConnect($config) { $mysqli = null; - $dbuser = $this->store->getVar(SYSTEM_DB_USER, STORE_SYSTEM); - $dbserver = $this->store->getVar(SYSTEM_DB_SERVER, STORE_SYSTEM); - $dbpw = $this->store->getVar(SYSTEM_DB_PASSWORD, STORE_SYSTEM); - $db = $this->store->getVar(SYSTEM_DB_NAME, STORE_SYSTEM); - - $mysqli = new \mysqli($dbserver, $dbuser, $dbpw, $db); + $mysqli = new \mysqli($config[SYSTEM_DB_SERVER], $config[SYSTEM_DB_USER], $config[SYSTEM_DB_PASSWORD], $config[SYSTEM_DB_NAME]); if ($mysqli->connect_error) { - throw new UserFormException ("Error open Database 'mysql:host=" . $dbserver . ";dbname=" . $db . ";dbuser=" . $dbuser . "'': " . $mysqli->connect_errno . PHP_EOL . $mysqli->connect_error, ERROR_DB_OPEN); + throw new UserFormException ("Error open Database 'mysql:host=" . $config[SYSTEM_DB_SERVER] . ";dbname=" . $config[SYSTEM_DB_NAME] . ";dbuser=" . $config[SYSTEM_DB_USER] . "'': " . $mysqli->connect_errno . PHP_EOL . $mysqli->connect_error, ERROR_DB_OPEN); } return $mysqli; @@ -239,8 +248,10 @@ class Database { $result = 0; $stat = array(); - $this->store->setVar(SYSTEM_SQL_FINAL, $sql, STORE_SYSTEM); - $this->store->setVar(SYSTEM_SQL_PARAM_ARRAY, $parameterArray, STORE_SYSTEM); + if ($this->store !== null) { + $this->store->setVar(SYSTEM_SQL_FINAL, $sql, STORE_SYSTEM); + $this->store->setVar(SYSTEM_SQL_PARAM_ARRAY, $parameterArray, STORE_SYSTEM); + } // Logfile $this->dbLog($sqlLogMode, $sql, $parameterArray); @@ -308,7 +319,9 @@ class Database { break; } - $this->store->setVar(SYSTEM_SQL_COUNT, $count, STORE_SYSTEM); + if ($this->store !== null) { + $this->store->setVar(SYSTEM_SQL_COUNT, $count, STORE_SYSTEM); + } // Logfile $this->dbLog($sqlLogMode, $msg); @@ -347,7 +360,7 @@ class Database { $status = ''; - $sqlLogMode = $this->store->getVar(SYSTEM_SQL_LOG_MODE, STORE_SYSTEM); + $sqlLogMode = ($this->store === null) ? $this->store->getVar(SYSTEM_SQL_LOG_MODE, STORE_SYSTEM) : SQL_LOG_MODE_ERROR; switch ($mode) { case SQL_LOG_MODE_ALL: @@ -367,7 +380,7 @@ class Database { } // Client IP Address - $remoteAddress = $this->store->getVar(CLIENT_REMOTE_ADDRESS, STORE_CLIENT); + $remoteAddress = ($this->store === null) ? $this->store->getVar(CLIENT_REMOTE_ADDRESS, STORE_CLIENT) : '0.0.0.0'; $msg = '[' . date('Y.m.d H:i:s O') . '][' . $remoteAddress . ']'; diff --git a/extension/qfq/qfq/QuickFormQuery.php b/extension/qfq/qfq/QuickFormQuery.php index 92d406d7b083e832b948c1f2dd711bfc67381198..5304b24eb2774bc98f77a0de27c93ac5240d06be 100644 --- a/extension/qfq/qfq/QuickFormQuery.php +++ b/extension/qfq/qfq/QuickFormQuery.php @@ -286,6 +286,7 @@ class QuickFormQuery { case FORM_UPDATE: $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD); + // data['form-update']=.... $data = $build->process($formMode); $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD); break; @@ -336,6 +337,7 @@ class QuickFormQuery { $formAction->elements($rc, $this->feSpecAction, FE_TYPE_SENDMAIL); // Retrieve FE Values as JSON + // $data['form-update']=... $data = $build->process($formMode, $htmlElementNameIdZero); break; @@ -344,6 +346,7 @@ class QuickFormQuery { } if (is_array($data)) { + // $data['element-update']=... $data = $this->groupElementUpdateEntries($data); } return $data; diff --git a/extension/qfq/qfq/form/TypeAhead.php b/extension/qfq/qfq/form/TypeAhead.php new file mode 100644 index 0000000000000000000000000000000000000000..87545b6cab77607f3235ae870973ea98bae844bd --- /dev/null +++ b/extension/qfq/qfq/form/TypeAhead.php @@ -0,0 +1,110 @@ +<?php +/** + * Created by PhpStorm. + * User: crose + * Date: 3/13/17 + * Time: 9:29 PM + */ + +namespace qfq; + +use TYPO3\CMS\Core\FormProtection\Exception; + +require_once(__DIR__ . '/../store/Sip.php'); +//require_once(__DIR__ . '/store/FillStoreForm.php'); +require_once(__DIR__ . '/../store/Session.php'); +require_once(__DIR__ . '/../Constants.php'); +//require_once(__DIR__ . '/Save.php'); +//require_once(__DIR__ . '/helper/KeyValueStringParser.php'); +require_once(__DIR__ . '/../helper/Ldap.php'); +//require_once(__DIR__ . '/helper/HelperFormElement.php'); +//require_once(__DIR__ . '/exceptions/UserFormException.php'); +//require_once(__DIR__ . '/exceptions/CodeException.php'); +//require_once(__DIR__ . '/exceptions/DbException.php'); +//require_once(__DIR__ . '/exceptions/ErrorHandler.php'); +require_once(__DIR__ . '/../Database.php'); +//require_once(__DIR__ . '/Evaluate.php'); +//require_once(__DIR__ . '/BuildFormPlain.php'); +//require_once(__DIR__ . '/BuildFormTable.php'); +//require_once(__DIR__ . '/BuildFormBootstrap.php'); +//require_once(__DIR__ . '/report/Report.php'); +//require_once(__DIR__ . '/BodytextParser.php'); +//require_once(__DIR__ . '/Delete.php'); +//require_once(__DIR__ . '/form/FormAction.php'); + + +class TypeAhead { + + /** + * @var Database instantiated class + */ + protected $db = null; + + /** + * @var array + */ + protected $vars = array(); + + /** + * + */ + public function __construct($phpUnit = false) { + + if (!isset($_GET[TYPEAHEAD_API_QUERY]) || !isset($_GET[TYPEAHEAD_API_SIP])) { + throw new CodeException('Missing GET parameter "' . TYPEAHEAD_API_SIP . '" or "' . TYPEAHEAD_API_QUERY . '"'); + } + + $this->vars[TYPEAHEAD_API_QUERY] = $_GET[TYPEAHEAD_API_QUERY]; + $this->vars[TYPEAHEAD_API_SIP] = $_GET[TYPEAHEAD_API_SIP]; + + $session = Session::getInstance($phpUnit); + + $this->db = new Database(); + + } + + /** + * @return array|int + * @throws CodeException + * @throws DbException + * @throws UserFormException + */ + public function process() { + + $arr = array(); + $values = array(); + + $sipClass = new Sip(); + + $sipVars = $sipClass->getVarsFromSip($this->vars[TYPEAHEAD_API_SIP]); + + if (isset($sipVars[FE_TYPEAHEAD_SQL])) { + $arr = $this->typeAheadSql($sipVars, $this->vars[TYPEAHEAD_API_QUERY]); + } elseif (isset($sipVars[FE_TYPEAHEAD_LDAP_SERVER])) { + $ldap = new Ldap(); + $arr = $ldap->process($sipVars, $this->vars[TYPEAHEAD_API_QUERY]); + } + + return $arr; + } + + /** + * @param array $config + * @param $query + * @return array|int + * @throws CodeException + * @throws DbException + */ + private function typeAheadSql(array $config, $query) { + $values = array(); + + $query = '%' . $query . '%'; + $cnt = substr_count($config[FE_TYPEAHEAD_SQL], '?'); + for ($ii = 0; $ii < $cnt; $ii++) { + $values[] = $query; + } + + return $this->db->sql($config[FE_TYPEAHEAD_SQL], ROW_REGULAR, $values); + } + +} \ No newline at end of file diff --git a/extension/qfq/qfq/helper/KeyValueStringParser.php b/extension/qfq/qfq/helper/KeyValueStringParser.php index b44f9ab9c46f7b682ebd433e46a51d690f0c8df6..35c0f5fc760c48af121047eaa6784ae1b66071de 100644 --- a/extension/qfq/qfq/helper/KeyValueStringParser.php +++ b/extension/qfq/qfq/helper/KeyValueStringParser.php @@ -123,7 +123,7 @@ class KeyValueStringParser { if (count($keyValueArray) === 2) { // "a:1", "a:" - $returnValue[$key] = self::removeSourroundingQuotes(trim($keyValueArray[1])); + $returnValue[$key] = self::quoteUnwrap(trim($keyValueArray[1])); } else { // no Value given: "a" $returnValue[$key] = ($valueMode === KVP_VALUE_GIVEN) ? "" : $key; @@ -137,7 +137,7 @@ class KeyValueStringParser { * @param $string * @return string */ - private static function removeSourroundingQuotes($string) { + public static function quoteUnwrap($string) { $quotes = ['\'', '"']; if ($string === "" || strlen($string) === 1) { @@ -147,7 +147,89 @@ class KeyValueStringParser { if (in_array($string[0], $quotes) === true && self::isFirstAndLastCharacterIdentical($string)) { return substr($string, 1, strlen($string) - 2); } - return $string; } + + /** + * Works like PHP 'explode()', but respects $delimeter wrapped in ticks (single or double): those are not interpreted as delimiter. + * + * E.g.: "a,b,'c,d',e" with delimiter ',' will result in [ 'a', 'b', 'c,d', 'e' ] + * + * @param $delimeter + * @param $str + * @param int $limit + * @return array|bool + * @throws CodeException + */ + public static function explodeWrapped($delimeter, $str, $limit=PHP_INT_MAX ) { + + if($delimeter=='') { + return false; + } + + if($limit<0) { + throw new CodeException("Not Implemented: limit<0", ERROR_NOT_IMPLEMENTED); + } + + if($limit==0) { + $limit = 1; + } + + $final = array(); + $startToken=''; + $onHold = ''; + + $cnt = 0; + $arr = explode($delimeter, $str, PHP_INT_MAX); + foreach($arr as $value) { + $trimmed=trim($value); + if($value=='' && $startToken=='') { + + if($cnt<$limit) { + $final[] = ''; + $cnt++; + } + continue; + } + + if($startToken=='') { + switch ($trimmed[0]) { + case SINGLE_TICK: + case DOUBLE_TICK: + if($trimmed[0] == substr($trimmed, -1)) { + break; // In case start and end token is in one exploded item + } + $startToken = $trimmed[0]; + $onHold = $value; + continue 2; + default: + break; + } + + if($cnt>=$limit) { + $final[$cnt-1] .= $delimeter . $value; + } else { + $final[] = $value; + $cnt++; + } + continue; + } else { + $onHold .= $delimeter . $value; + $lastChar = substr($trimmed,-1); + if($startToken == $lastChar) { + + if($cnt>=$limit) { + $final[$cnt-1] .= $delimeter . $onHold; + } else { + $final[] = $onHold; + $cnt++; + } + $startToken = ''; + $onHold = ''; + } + } + } + + return $final; + } } diff --git a/extension/qfq/qfq/helper/Ldap.php b/extension/qfq/qfq/helper/Ldap.php new file mode 100644 index 0000000000000000000000000000000000000000..ced53b734c2b9110dcce909fe083281469c01a2c --- /dev/null +++ b/extension/qfq/qfq/helper/Ldap.php @@ -0,0 +1,119 @@ +<?php +/** + * Created by PhpStorm. + * User: crose + * Date: 3/14/17 + * Time: 11:32 PM + */ + +namespace qfq; + +use qfq; + +require_once(__DIR__ . '/KeyValueStringParser.php'); +require_once(__DIR__ . '/OnArray.php'); + +class Ldap { + + /** + * @param $query + * @return array + * @throws UserFormException + */ + public function process($config, $query) { + $arr = array(); + + $ldapServer = $config[FE_TYPEAHEAD_LDAP_SERVER]; + $ldapBaseDn = $config[FE_TYPEAHEAD_LDAP_BASE_DN]; + $ldapSearch = $config[FE_TYPEAHEAD_LDAP_SEARCH]; + $ldapSearch = str_replace('?', $query, $ldapSearch); + $ldapLimit = $config[FE_TYPEAHEAD_LIMIT]; + + $ds = ldap_connect($ldapServer); // must be a valid LDAP server! + if (!$ds) { + throw new UserFormException("Unable to connect to LDAP server: $ldapServer", ERROR_LDAP_CONNECT); + } + + $keyArr = $this->printfPrepare($config[FE_TYPEAHEAD_LDAP_KEY_PRINTF], $keyFormat); + $valueArr = $this->printfPrepare($config[FE_TYPEAHEAD_LDAP_VALUE_PRINTF], $valueFormat); + + $attr = array_values(array_unique(array_merge($keyArr, $valueArr))); + + // 'Size Limit errors' are reported, even if it is not a real problem. + // Fake all errors at the moment. + // TODO: just drop the 'Size Limit errors' and report all others + set_error_handler(function () { /* ignore errors */ + }); + $sr = ldap_search($ds, $ldapBaseDn, $ldapSearch, $attr, 0, $ldapLimit); + restore_error_handler(); + + $info = ldap_get_entries($ds, $sr); + + for ($i = 0; $i < $info["count"]; $i++) { + + $key = $this->printfResult($keyFormat, $keyArr, $info[$i]); + $value = $this->printfResult($valueFormat, $valueArr, $info[$i]); + + if ($key == '' || $value == '') { + continue; // if $key or $value is empty: skip + } + + $arr[] = [API_TYPEAHEAD_KEY => $key, API_TYPEAHEAD_VALUE => $value]; + } + + ldap_close($ds); + + return $arr; + } + + /** + * Very specific function to prepare the later 'printfResult()'. + * + * @param $fmtComplete + * @param $fmtFirst + * @return mixed + * @throws CodeException + * @throws UserFormException + */ + private function printfPrepare($fmtComplete, &$fmtFirst) { + + // Typical $fmtComplete: "'%s / %s / %s', cn, mail. telephonenumber" + $arr = KeyValueStringParser::explodeWrapped(',', $fmtComplete); + + if (count($arr) < 2) { + throw new UserFormException("Expect a sprintf compatible format string with a least one argument. Got: '" . $fmtComplete . "'", ERROR_MISSING_PRINTF_ARGUMENTS); + } + + // unquote and return the part printf-'formatString' + $fmtFirst = trim($arr[0], SINGLE_TICK . DOUBLE_TICK); + + array_shift($arr); // remove first entry: + + $arr = OnArray::trimArray($arr); // remove any leading/trailing spaces + + // toLower is important, cause the LDAP attribute names are all lowercase in PHP - if the user specifies in CamelHook , the vars are not found. + return OnArray::arrayValueToLower($arr); + + } + + /** + * Plays sprintf with supplied arguments. Collect the values of the arguments in the array + * $keyArr to pass them via 'call_user_func_array' to sprintf. + * + * @param $format + * @param $infoElement + * @return string output of sprintf + * @throws CodeException + * @throws UserFormException + */ + private function printfResult($format, array $keyArr, $infoElement) { + + $args = array($format); + + foreach ($keyArr as $key) { + $args[] = (isset($infoElement[$key][0])) ? $infoElement[$key][0] : ''; + } + + return call_user_func_array('sprintf', $args); + } +} \ No newline at end of file diff --git a/extension/qfq/qfq/helper/OnArray.php b/extension/qfq/qfq/helper/OnArray.php index 54b48b57f16c8e9b6566e6803690e22c7a29dd64..7e2806b920b6df2c01fba8e074917b228f76fd00 100644 --- a/extension/qfq/qfq/helper/OnArray.php +++ b/extension/qfq/qfq/helper/OnArray.php @@ -29,6 +29,7 @@ class OnArray { * @param array $dataArray * @param string $keyValueGlue * @param string $rowGlue + * @param string $encloseValue - char (or string) to enclose the value with. * @return string */ public static function toString(array $dataArray, $keyValueGlue = '=', $rowGlue = '&', $encloseValue = '') { @@ -73,7 +74,7 @@ class OnArray { * @param string $character_mask * @return array */ - public static function trimArray(array $arr, $character_mask) { + public static function trimArray(array $arr, $character_mask = " \t\n\r\0\x0B") { foreach ($arr as $key => $item) { $arr[$key] = trim($item, $character_mask); } @@ -167,4 +168,13 @@ class OnArray { return $new; } + + public static function arrayValueToLower(array $arr) { + $new = array(); + + foreach ($arr as $key => $value) { + $new[$key] = strtolower($value); + } + return $new; + } } \ No newline at end of file diff --git a/extension/qfq/qfq/helper/Support.php b/extension/qfq/qfq/helper/Support.php index 7526a468bc2e53cd57b2fe609e96bbd0464aa137..1fc32d719d2c8725828dc71f385da52c417c36f8 100644 --- a/extension/qfq/qfq/helper/Support.php +++ b/extension/qfq/qfq/helper/Support.php @@ -625,6 +625,9 @@ class Support { } /** + * Looks in $arr if there is an element $index. If not, set it to $value. + * If $overwriteThis!=false, replace the the original value with $value, if $arr[$index]==$overwriteThis. + * * @param array $arr * @param string $index * @param string $value @@ -692,6 +695,8 @@ class Support { } /** + * Convert 'false' and 'empty' to '0'. + * * @param $val * @return string */ diff --git a/extension/qfq/qfq/store/Config.php b/extension/qfq/qfq/store/Config.php index fe646b5ebbc39cb0d2d9370da61e9aa758b14fb2..92316a589377b8913907fc80b3de88b18a18e03d 100644 --- a/extension/qfq/qfq/store/Config.php +++ b/extension/qfq/qfq/store/Config.php @@ -17,10 +17,10 @@ class Config { /** * Read config.qfq.ini. * - * @throws CodeException - * @throws qfq\UserFormException + * @param string $fileConfigIni + * @return array + * @throws UserFormException */ - public function readConfig($fileConfigIni = '') { if ($fileConfigIni == '') { diff --git a/extension/qfq/sql/formEditor.sql b/extension/qfq/sql/formEditor.sql index 0cc7dc427c9f2422a5479f6b063d0eb398f654e0..3269b01c30a02629d5fa79149c0019472b169491 100644 --- a/extension/qfq/sql/formEditor.sql +++ b/extension/qfq/sql/formEditor.sql @@ -90,7 +90,7 @@ CREATE TABLE IF NOT EXISTS `FormElement` ( `rowLabelInputNote` SET('row', 'label', '/label', 'input', '/input', 'note', '/note', '/row') NOT NULL DEFAULT 'row,label,/label,input,/input,note,/note,/row', `note` TEXT NOT NULL, `tooltip` VARCHAR(255) NOT NULL DEFAULT '', - `placeholder` VARCHAR(255) NOT NULL DEFAULT '', + `placeholder` VARCHAR(512) NOT NULL DEFAULT '', `value` TEXT NOT NULL, `sql1` TEXT NOT NULL, @@ -212,7 +212,7 @@ VALUES ''), (2, 'formId', 'formId', 'readonly', 'text', 'all', 'native', 110, 0, 255, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), (2, 'feIdContainer', 'Container', 'show', 'select', 'all', 'native', 120, 0, 0, '', '', '', - '{{!SELECT fe.id, CONCAT(fe.class, " / ", fe.label) FROM FormElement As fe WHERE fe.formId={{formId:S0}} AND fe.class="container" ORDER BY fe.ord }}', + '{{!SELECT fe.id, CONCAT(fe.type, " / ", fe.label) FROM FormElement As fe WHERE fe.formId={{formId:S0}} AND fe.class="container" ORDER BY fe.type, fe.name }}', 'emptyItemAtStart', 100, '', 'no', '', '', '', '', ''), (2, 'enabled', 'Enabled', 'show', 'checkbox', 'all', 'native', 130, 0, 0, '', '', '', '', '', 100, '', 'no', '', '', '', '', ''), (2, 'dynamicUpdate', 'Dynamic Update', 'show', 'checkbox', 'all', 'native', 135, 0, 0, 'On change, this element will be updated and trigger other.', @@ -242,7 +242,7 @@ VALUES (2, 'maxLength', 'Maxlength', 'show', 'text', 'all', 'native', 450, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), (2, 'note', 'Note', 'show', 'editor', 'all', 'native', 460, '', 255, '', '', '', '', 'editor-plugins=code link table textcolor textpattern\neditor-toolbar=code | styleselect link table | bullist numlist | forecolor backcolor bold italic\neditor-menubar=false\neditor-statusbar=false', 102, '', 'no', '', '', '', '', ''), (2, 'tooltip', 'Tooltip', 'show', 'text', 'all', 'native', 470, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), - (2, 'placeholder', 'Placeholder', 'show', 'text', 'all', 'native', 480, 0, 255, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), + (2, 'placeholder', 'Placeholder', 'show', 'text', 'all', 'native', 480, 0, 0, '', '', '', '', '', 102, '', 'no', '', '', '', '', ''), (2, 'value', 'value', 'show', 'text', 'all', 'native', 500, '40,2', 255, '', '', '', '', '', 103, '', 'no', '', '', '', '', ''), (2, 'sql1', 'sql1', 'show', 'text', 'all', 'native', 510, '40,5', 255, 'MariaDB: <a href="https://mariadb.com/kb/en/mariadb/select/">Select</a>, <a href="https://mariadb.com/kb/en/mariadb/functions-and-operators/">Functions</a>', '', '', '', '', 103, '', 'no', '', '', '', '', ''), (2, 'parameter', 'Parameter', 'show', 'text', 'all', 'native', 520, '40,4', 255, '', diff --git a/extension/qfq/tests/phpunit/KeyValueStringParserTest.php b/extension/qfq/tests/phpunit/KeyValueStringParserTest.php index bebcb8339347d42fe821179e75c2c71a767a4098..c19b1c48be886cd87467283a761f5384a5742c3a 100644 --- a/extension/qfq/tests/phpunit/KeyValueStringParserTest.php +++ b/extension/qfq/tests/phpunit/KeyValueStringParserTest.php @@ -169,4 +169,85 @@ class KeyValueStringParserTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($expected, $actual); $this->assertCount(2, $actual); } + + public function testExplodeContent() { + $actual = keyValueStringParser::explodeWrapped('', ''); + $this->assertEquals(false, $actual); + + $actual = keyValueStringParser::explodeWrapped(':', ''); + $this->assertEquals([''], $actual); + + $actual = keyValueStringParser::explodeWrapped(':', 'a,b,c'); + $this->assertEquals(['a,b,c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', 'a,b,c'); + $this->assertEquals(['a' , 'b' ,'c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', ' a,b,c'); + $this->assertEquals([' a' , 'b' ,'c'], $actual); + $actual = keyValueStringParser::explodeWrapped(',', 'a ,b,c'); + $this->assertEquals(['a ' , 'b' ,'c'], $actual); + $actual = keyValueStringParser::explodeWrapped(',', ' a ,b,c'); + $this->assertEquals([' a ' , 'b' ,'c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', 'a, b,c'); + $this->assertEquals(['a' , ' b' ,'c'], $actual); + $actual = keyValueStringParser::explodeWrapped(',', 'a,b ,c'); + $this->assertEquals(['a' , 'b ' ,'c'], $actual); + $actual = keyValueStringParser::explodeWrapped(',', 'a, b ,c'); + $this->assertEquals(['a' , ' b ' ,'c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', 'a,b, c'); + $this->assertEquals(['a' , 'b' ,' c'], $actual); + $actual = keyValueStringParser::explodeWrapped(',', 'a,b,c '); + $this->assertEquals(['a' , 'b' ,'c '], $actual); + $actual = keyValueStringParser::explodeWrapped(',', 'a,b, c '); + $this->assertEquals(['a' , 'b' ,' c '], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', 'a,"b",c'); + $this->assertEquals(['a' , '"b"','c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', 'a,"b,b",c'); + $this->assertEquals(['a' , '"b,b"','c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "a,'b',c"); + $this->assertEquals(['a' , "'b'",'c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "a,'b,b',c"); + $this->assertEquals(['a' , "'b,b'",'c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "a,'b,b,b',c"); + $this->assertEquals(['a' , "'b,b,b'",'c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "'a,a,a,a','b','c,c,c,c,c'"); + $this->assertEquals(["'a,a,a,a'" , "'b'","'c,c,c,c,c'"], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', " 'a,a,a' , 'b' , 'c,c' "); + $this->assertEquals([" 'a,a,a' " , " 'b' "," 'c,c' "], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', 'a,b,c', 2); + $this->assertEquals(['a' , 'b,c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "'a,a',b,c", 2); + $this->assertEquals(["'a,a'" , 'b,c'], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "a,'b,b',c", 2); + $this->assertEquals(['a' , "'b,b',c" ], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "a,b,'c,c'", 2); + $this->assertEquals(['a' , "b,'c,c'" ], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "a,b,c", 0); + $this->assertEquals(['a,b,c' ], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "a,b,c", 1); + $this->assertEquals(['a,b,c' ], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "'a,b',c", 1); + $this->assertEquals(["'a,b',c" ], $actual); + + $actual = keyValueStringParser::explodeWrapped(',', "a,'b,c'", 1); + $this->assertEquals(["a,'b,c'" ], $actual); + + } } diff --git a/less/qfq-bs.css.less b/less/qfq-bs.css.less index a148d86deafe77c037b6715db51d54edfa2efc53..e1f88f2af51d35b9e36f78fa9d726f20448f20e0 100644 --- a/less/qfq-bs.css.less +++ b/less/qfq-bs.css.less @@ -129,6 +129,32 @@ i.@{spinner_class} { .qfq-note { padding-top: 7px; } + +// typeAhead Input: Default Bootstrap column width +.twitter-typeahead { + display: block !important; +} + +// TypeAhead Suggestions +.tt-menu { + padding: 12px; + background-color: #fff; + border-left: 1px solid #66afe9; + border-right: 1px solid #66afe9; + border-bottom: 1px solid #66afe9; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6) +} + +.tt-suggestion + .tt-suggestion { + margin-top: 12px; +} + + + + // Mit BB anschauen wie man die NOTE Felder formatiert // //.text-input { diff --git a/qfq.zip b/qfq.zip index 95af054c1c89ad18441eff8216d5a7b2c274d9a5..ce2723186b9902b827f1b6559ea30660ffaf375e 100644 Binary files a/qfq.zip and b/qfq.zip differ