Commit 9679bf99 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Documentation/UsersManual/index.rst: T3 Bodytext 'join lines' and 'nested' structures described.

Documentation/index.rst: Version number increased to 0.2, added RO as Author (accidently removed,
BodyTextParser: implemented for 'join' and 'nested'
parent eecfb808
......@@ -35,10 +35,10 @@ QFQ Extension
2016
:Author:
Carsten Rose
Carsten Rose, Rafael Ostertag
:Email:
carsten.rose@math.uzh.ch
carsten.rose@math.uzh.ch, rafael.ostertag@math.uzh.ch
:License:
This document is published under the Open Publication License
......
......@@ -603,6 +603,8 @@ Class: Native
+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+
| emptyItemAtEnd | | | | | | | | | | - | | | | |
+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+
| emptyHide | | | | | | | | | | - | | | | |
+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+
| accept | | | | | | | | | | | | | | - 3 |
+------------------+----------+---------+-------------+----------+--------+-------+------+----------+-------+--------+-----------+----------+---------+--------+
......@@ -698,6 +700,8 @@ Type: radio
* '''emptyItemAtStart''': Existence of this item inserts an empty entry at the beginning of the selectlist.
* '''emptyItemAtEnd''': Existence of this item inserts an empty entry at the end of the selectlist.
* '''emptyHide''': Existence of this item hides the empty entry. This is usefull for e.g. Enums, which have a en empty
entry and the empty value should not be an option to be selected.
Type: select
^^^^^^^^^^^^^^^^^^^^
......@@ -727,6 +731,8 @@ Type: select
* '''emptyItemAtStart''': Existence of this item inserts an empty entry at the beginning of the selectlist.
* '''emptyItemAtEnd''': Existence of this item inserts an empty entry at the end of the selectlist.
* '''emptyHide''': Existence of this item hides the empty entry. This is usefull for e.g. Enums, which have a en empty
entry and the empty value should not be an option to be selected.
Type: subrecord
^^^^^^^^^^^^^^^
......@@ -855,15 +861,16 @@ A simple example
^^^^^^^^^^^^^^^^
Assume that the database has a table person with columns first_name and last_name. To create a simple list of all persons, we can do the following:
::
10.sql = SELECT id AS person_id, CONCAT(first_name, " ", last_name, " ") 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:
::
Marc MusterElton JohnSpeedy Gonzales
..
......@@ -876,26 +883,17 @@ However, we can modify (wrap) the output by setting the values of various ?keys
::
10.sql = SELECT id AS person_id, CONCAT(first_name, " ", last_name, " ") AS name FROM person
10.sep = <br />
Marc Muster<br />Elton John<br />Speedy Conzales<br />
..
which gives us the desired simple list (we use linebreaks for simplicity here) when displayed by a browser:
HTML output:
::
Marc Muster
Elton John
Speedy Conzales
Marc Muster<br />Elton John<br />Speedy Conzales<br />
..
Syntax
------
......@@ -955,8 +953,6 @@ See the example below:
..
This would result in
::
......@@ -968,26 +964,76 @@ This would result in
..
Across several lines
^^^^^^^^^^^^^^^^^^^^
To make SQL quieres more readable, it's possible to split a line across several lines. Lines with keywords are on their
own - if there are non 'keyword' lines, they will be appended at the last keyword line. 'Keyword' lines:
* <level>.<keyword> =
* {
* <level> {
Example::
10.sql = SELECT 'hello world'
FROM mastertable
10.tail = End
20.sql = SELECT 'a warm welcome'
'some additional', 'columns'
FROM smartTable
WHERE id>100
20.head = <h3>
20.tail = </h3>
Nesting of levels
^^^^^^^^^^^^^^^^^
Levels can be nested by using curly brackets::
10.sql = SELECT 'hello world'
20 {
sql = SELECT 'a new query'
head = <h1>
tail = </h1>
}
30 {
sql = SELECT 'a third query'
head = <h1>
tail = </h1>
40 {
sql = SELECT 'a nested nested query'
}
}
30.40.tail = End
50
{
sql = SELECT 'A query with braces on their own'
}
Columns of the upper level result can be accessed via variables, eg. {{10.person_id}} will be replaced by the value in the person_id column.
+-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|**Levels** |A report is divided into levels. Example 1 has 3 levels **10**, **20.20**, **20.30.10** |
|**Levels** |A report is divided into levels. Example 1 has 3 levels **10**, **20.25**, **20.25.10** |
+-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|**Qualifier**|A level is divided into qualifiers **20.30.10** has 3 qualifiers **20**, **30**, **10** |
+-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|**Root |Is a level with one qualifier. Example 1 has 2 root levels *10* and *20*. |
|**Root |Is a level with one qualifier. E.g.: 10 |
|levels** | |
+-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|**Sub |Is a level with more than one qualifier. Example 1 has 2 sub levels **20.20** and **20.30.10** |
|**Sub |Is a level with more than one qualifier. E.g. levels **20.25** and **20.30.10** |
|levels** | |
+-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|**Child** |The level **20** has one child **20.20** |
|**Child** |The level **20** has one child **20.25** |
+-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|**Parent** |The level 20.20 has a parent **20** |
|**Parent** |The level 20.25 has a parent **20** |
+-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|**Example |**10** and **20** is a root level and will be executed independently. **10** don't have a sub level. **20.20** will be executed as many times as **20** has row numbers. **20.30.10** won't be executed because there isn't |
|**Example |**10** and **20** are root level and will be executed independently. **10** don't have a sub level. **20.25** will be executed as many times as **20** has row numbers. **20.30.10** won't be executed because there isn't |
|explanation**|any **20.30** level |
+-------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
......@@ -1004,7 +1050,7 @@ Report Example 1:
20.sql = SELECT p.id AS p_id, p.first_name, " - ", p.last_name FROM person AS p WHERE p.typ LIKE "student"
# Show all the marks from the current student ordered chronological
20.20.sql = SELECT e.mark FROM exam AS e WHERE e.p_id={{20.p_id}} ORDER BY e.date
20.25.sql = SELECT e.mark FROM exam AS e WHERE e.p_id={{20.p_id}} ORDER BY e.date
# This query will never be fired, cause there is no direct parent called 20.30.
20.30.10.sql = SELECT 'never fired'
......
......@@ -19,4 +19,4 @@ CSS_LINK_CLASS_INTERNAL = internal
CSS_LINK_CLASS_EXTERNAL = external
; QFQ with own Bootstrap: 'container'. QFQ already nested in Bootstrap of mainpage: <empty>
CSS_CLASS_MAIN_CONTAINER =
CSS_CLASS_QFQ_CONTAINER =
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 3/18/16
* Time: 5:43 PM
*/
namespace qfq;
class BodytextParser {
/**
* @param $bodytext
* @return mixed
*/
public function process($bodytext) {
$bodytext = $this->stripAndRemoveComment($bodytext);
$bodytext = $this->joinLine($bodytext);
$bodytext = $this->unNest($bodytext);
$bodytext = $this->stripAndRemoveComment($bodytext);
return $bodytext;
}
/**
* Trim all lines, remove all empty lines and all lines which start with '#'
* @param $bodytext
* @return string
*/
private function stripAndRemoveComment($bodytext) {
$data = array();
$src = explode(PHP_EOL, $bodytext);
foreach ($src as $row) {
$row = trim($row);
if ($row === '' || $row[0] === '#') {
continue;
}
$data[] = $row;
}
return implode(PHP_EOL, $data);
}
//PREG_SPLIT_DELIM_CAPTURE
/**
* Join lines, which do not begin with '<level>.<keyword>[ ]='
*
* @param array $bodytextArray
* @return string
*/
private function joinLine($bodytext) {
$data = array();
$bodytextArray = explode(PHP_EOL, $bodytext);
$full = '';
foreach ($bodytextArray as $row) {
// Valid 'new line' starts indicators: form, <level>, <level.sublevel>, <level>.<keyword>, {, <level> {, }
if ((1 === preg_match('/^\s*(\d*(\.)?)*\s*(head|althead|tail|sql|rbeg|rend|renr|rsep|fbeg|fend|fsep|form) *=/', $row))
|| (1 === preg_match('/^\s*(\d*(\.)?)*\s*({|})\s*/', $row))
|| (1 === preg_match('/^\s*(\d+(\.)?)+/', $row))
) {
// if there is already something: save this
if ($full !== '')
$data[] = $full;
// start new line
$full = $row;
} else {
// continue row: concat
$full .= ' ' . $row;
}
}
// Save last line
if ($full !== '')
$data[] = $full;
return implode(PHP_EOL, $data);
}
private function unNest($bodytext) {
// Replace '\{' | '\}' by internal token. All remaining '}' | '{' means: 'nested'
$bodytext = str_replace('\{', '#&[_#', $bodytext);
$bodytext = str_replace('\}', '#&]_#', $bodytext);
$bodytext = str_replace('{{', '#&[[_#', $bodytext);
$bodytext = str_replace('}}', '#&]]_#', $bodytext);
$result = $bodytext;
$posFirstClose = strpos($result, '}');
while ($posFirstClose !== false) {
$posMatchOpen = strrpos(substr($result, 0, $posFirstClose), '{');
if ($posMatchOpen === false) {
throw new \qfq\UserException("Missing open delimiter: $result", ERROR_MISSING_OPEN_DELIMITER);
}
$pre = substr($result, 0, $posMatchOpen);
if ($pre === false)
$pre = '';
$post = substr($result, $posFirstClose + 1);
if ($post === false)
$post = '';
// trim also removes '\n'
$match = trim(substr($result, $posMatchOpen + 1, $posFirstClose - $posMatchOpen - 1));
// "10.sql = SELECT...\n20 {\n
$levelStartPos = strrpos(trim($pre), PHP_EOL);
$levelStartPos = ($levelStartPos === false) ? 0 : $levelStartPos + 1; // Skip PHP_EOL
$level = trim(substr($pre, $levelStartPos));
// if($level==='') {
// $pre=
// }
// remove 'level' from last line
$pre = substr($pre, 0, $levelStartPos);
// Split nested content in single rows
$lines = explode(PHP_EOL, $match);
foreach ($lines as $line) {
$pre .= $level . '.' . $line . PHP_EOL;
}
$result = $pre . $post;
$posFirstClose = strpos($result, '}');
}
$result = str_replace('#&[_#', '{', $result);
$result = str_replace('#&]_#', '}', $result);
$result = str_replace('#&[[_#', '{{', $result);
$result = str_replace('#&]]_#', '}}', $result);
return $result;
}
}
\ No newline at end of file
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 3/21/16
* Time: 9:29 AM
*/
namespace qfq;
//use qfq;
require_once(__DIR__ . '/../../qfq/BodytextParser.php');
require_once(__DIR__ . '/../../qfq/exceptions/UserException.php');
class BodytextParserTest extends \PHPUnit_Framework_TestCase {
public function testProcessPlain() {
$btp = new BodytextParser();
// Simple row, nothing to remove
$given = "10.sql = SELECT 'Hello World'";
$expected = $given;
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Several rows, remove all but one
$given = "\n#some comments\n10.sql = SELECT 'Hello World'\n\n \n #more comment";
$expected = "10.sql = SELECT 'Hello World'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Several rows, all to remove
$given = "\n#some comments\n\n\n \n #more comment";
$expected = "";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Join a line
$given = "\n10.sql = SELECT 'Hello World',\n'more content'\n WHERE help=1";
$expected = "10.sql = SELECT 'Hello World', 'more content' WHERE help=1";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Join several lines, incl. form
$given = "\n10.sql = SELECT 'Hello World',\n'more content'\n WHERE help=1\n 20.head = <table>\n 30.sql = SELECT\n col1,\n col2, \n col3\n # Query stops here\nform = Person\n";
$expected = "10.sql = SELECT 'Hello World', 'more content' WHERE help=1\n20.head = <table>\n30.sql = SELECT col1, col2, col3\nform = Person";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Nested expression: one
$given = "10{\nsql = SELECT 'Hello World'}";
$expected = "10.sql = SELECT 'Hello World'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Nested expression, one, added some white spaces
$given = "\n\n10 { \n \n sql = SELECT 'Hello World' \n\n }\n\n";
$expected = "10.sql = SELECT 'Hello World'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Nested expression: multiple, simple
$given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n}\n30 {\nsql='Hello world3'\n}\n";
$expected = "10.sql = SELECT 'Hello World'\n20.sql='Hello world2'\n30.sql='Hello world3'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Nested expression: complex
$given = "10.sql = SELECT 'Hello World'\n20 {\nsql='Hello world2'\n30 {\n sql=SELECT 'Hello World3'\n40 { sql = SELECT 'Hello World4'\n} \n}\n}";
$expected = "10.sql = SELECT 'Hello World'\n20.sql='Hello world2'\n20.30.sql=SELECT 'Hello World3'\n20.30.40.sql = SELECT 'Hello World4'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// form=...., {{ }}
$given = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30 { \nsql=SELECT 'Hello World'\n}\n form=Person\n";
$expected = "10.sql = SELECT 'Hello World'\nform = {{form:S}}\n20.sql = SELECT 'Hello World2'\n30.sql=SELECT 'Hello World'\nform=Person";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Nested: open bracket alone
$given = "10.sql = SELECT 'Hello World'\n20\n{\nhead=test\n}\n30.sql = SELECT 'Hello World'\n";
$expected = "10.sql = SELECT 'Hello World'\n20.head=test\n30.sql = SELECT 'Hello World'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
// Nested: unclosed open bracket
$given = "10.sql = SELECT 'Hello World'\n20 {\n30.sql = SELECT 'Hello World'\n";
$expected = "10.sql = SELECT 'Hello World'\n20 {\n30.sql = SELECT 'Hello World'";
$result = $btp->process($given);
$this->assertEquals($expected, $result);
}
/**
* @expectedException \qfq\UserException
*
*/
public function testProcessException() {
$btp = new BodytextParser();
// Nested: unclosed open bracket
$btp->process("10.sql = SELECT 'Hello World'\n } \n30.sql = SELECT 'Hello World'\n");
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment