Tutorial
Note
The tutorial is intended for new employees in the QFQ team.
Important Links
You should have received personal access credentials to the following resources.
phpMyAdmin (database) https://webwork22.math.uzh.ch/pma
Typo3 Backend https://webwork22.math.uzh.ch/[your*name]/typo3
Typo3 Backend (Solutions) https://webwork22.math.uzh.ch/qfqtutorial/typo3 kpweb: qfqtutorial (pma & t3)
Frontend (Solutions) https://webwork22.math.uzh.ch/qfqtutorial
Tutorial DB
Open phpMyAdmin and execute Tutorial.example.sql in your personal database. This will create the tables and data required for the upcoming tasks.
With MariaDB (a fork of MySQL) you can work directly via the command line, or use one of the many MySQL database frontends. In the QFQ team, phpMyAdmin is primarily used (link to the current installation see above).
For the introduction, we recommend using a frontend, not the command line.
SQL - Basic
Source DrawIO: tutorial-erd.drawio
Task 1 - SELECT
Output the names of all books.
Output the names of all books, except the book with the name “The Hobbit”.
Task 2 - JOIN, NOW, CONCAT
a) Output the names of all books in the first column and the last names of the authors in the second column.
b) Output a list of all current, non-overdue rentals with the name of the book, the name of the person currently borrowing it, and the due date (“end”). For a current (non-overdue) rental, the current time must be between “start” and “end” and the book must not yet have been returned (returned = 0).
c) Output a list containing in a single column all books like this: “The book ‘(Bookname)’ was written by ‘(Author Firstname)’ ‘(Author Lastname)’”
Task 3 - ORDER BY, LIMIT
Output the names of all books in reverse alphabetical order.
b) Sort the result of task 2b by due date (ascending). Show only the first three results.
Task 4 - COUNT, SUM
Output the number of books by the author “Dan Brown”.
Output the total number of pages of all books.
Task 5 - GROUP BY, LEFT JOIN, GROUP_CONCAT, NULL
a) In the first column, output the names of all books, and in the second column all rental dates of the book (“start”) separated by commas.
b) In the first column, output the names of all authors (persons who have written at least one book), and in the second column all works of the respective author (comma separated).
Task 6 - IF, DATE_FORMAT
a) In the first column, output the names of all persons. In the second column, output “yes” if the person has already borrowed a book. Otherwise, output “no”.
b) In the first column, output the names of all books. In the second column, output “available now” if the book is currently not borrowed. If the book is currently borrowed, then output “available from (dd.mm.yyyy)”.
Task 7 - INSERT
a) Insert a new book into the database. The name should be “New Book”, it has 100 pages and was written by the author with id 1. Write down or remember the id of the new book. Use the SQL INSERT statement. Do not use the phpMyAdmin Insert function.
Task 8 - UPDATE
a) Update the new book using the id remembered earlier. Set the name to “Revised New Book” and the number of pages to 101.
Change the author of all books to “J. R. R. Tolkien” (do not update via id).
Task 9 - DELETE
Delete everything from the Rental table.
Delete the book with the id from task 7.
Task 10 - Grp, Glue
Grp and Glue are not SQL keywords. They are a best practice we often use, see Multi purpose tables & column keys.
a) Look at the following diagram and think about what Grp and Glue could stand for. Replace Grp and Glue with other table names that would make sense in this context. (For now ignore that Grp has a recursive relation with itself.)
Source DrawIO: tutorial-erd.drawio
Why is the table called ‘Grp’ and not simply ‘Group’?
What do we generally achieve by using Grp and Glue?
Why does Grp have a ‘grId’ that points to another Grp?
Think of a scenario in which Grp is connected via Glue to the table ‘Person’.
Example of Grp and Glue interaction with tables Person and Book
Glue as a Bridge:
The ‘Glue’ table serves as a connection between the ‘Grp’ table and the ‘Person’ and ‘Book’ tables.
Understanding Glue Columns:
The ‘Glue.xId’ represents the id from either the ‘Person’ or ‘Book’ table, depending on which entity it is connecting.
The ‘Glue.grId’ corresponds to the ‘Grp.id’, creating the link between an entity (Person or Book) and a group.
Practical Example:
For instance, ‘Book_1’ (with ‘id:1’) is connected to ‘grId:2’, which in turn links to ‘Grp.id:2’, identifying it as part of the “Fantasy” genre.
Grp Hierarchy:
The ‘Grp’ table has a hierarchical structure where rows with ‘grId:0’ act as parents. The child records have their grId matching the id of the parent, establishing the parent-child relationship within the groups.
SQL - Advanced
Tip
GROUP BY
Always specify, if aggregate functions are used - if not, MySQL will do it, which can be different
from what is expected.
Performance
GROUP BY
If possible, use AND to minimize resultset.
Do not use GROUP BY if you are not understanding why it is necessary.
SUBSELECT()
Remember: for each selected row of the outer SELECT, the subselect is fired.
Example: Outer SELECT has 100 rows, than 101 queries are fired.
Warning
SELECT p.id FROM Person AS p WHERE p.id in (SELECT adr.pId FROM Address AS adr WHERE adr.pId=p.id)
This is a huge waste of database performance!
Use the same CHARACTER SET and COLLATION on all of your tables. If they are mixed, the indexes can still be used, but MariaDB have to convert it/reread - which practicaly means a deep impact.
Note
ALTER TABLE <table> CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
Prerequisites Web Development
CMS / Typo3
You will receive an introduction to Typo3; specifically, the following points should be conveyed to you:
What is a CMS?
Pages (Title, Page Slug, Hide in Menu), PageContent
FeUser, BeUser
Permissions
HTML Basics
It is assumed that you are already familiar with the basics of HTML. This HTML section is intended to refresh your knowledge.
Task 1 - HTML Table
Create a new Typo3 page and add a Page Content section of type HTML. In it, create the following table using pure HTML (no CSS styling), to refresh your knowledge.
Task 2 - Self-study
This is a list of HTML topics and skills. Work through the list. What you already know well, you can skip. If you are not familiar with something, look it up on the Internet (e.g. https://www.w3schools.com/html/ ) and try it directly in your own HTML Page Content!
HTML Editors
HTML Basic
HTML Elements
HTML Attributes
HTML Headings
HTML Paragraphs
HTML Styles
HTML Formatting
HTML Quotations
HTML Comments
HTML CSS
HTML Links
HTML Images
HTML Tables
HTML Lists
HTML Blocks
HTML Classes
HTML JavaScript
HTML File Paths
HTML Head
HTML Layout
HTML Responsive
HTML Computercode
HTML Entities
HTML Symbols
HTML URL Encode
HTML Forms
HTML FormElements
HTML Input Types
HTML Input Attributes
HTML Examples
HTML Quiz
HTML Exercises
HTML Certificate
HTML Summary
Skills
Text formatting (Heading, Bold, Italic)
Links:
Fully qualified
Relative
Parameters
Anchor
CSS / Bootstrap
It is assumed that you already have basic knowledge of CSS. If not, you can work through it in self-study. For example, there is a CSS tutorial at https://www.w3schools.com/css/
Instead of writing your own CSS classes or even individual styles per element, we use Bootstrap wherever possible, or the CSS classes delivered with QFQ (QFQ CSS Classes).
Bootstrap is a CSS/JavaScript library that provides a set of unified styles for various elements (tables, buttons, etc.). Bootstrap also includes complete components, such as a modal pop-up dialog or progress bars.
You do not need to worry about including Bootstrap – within your Typo3 instance, Bootstrap is already available. Pay attention to the currently integrated BS version and the corresponding documentation.
Task 1 - Bootstrap Grid
Another important aspect of Bootstrap is that it makes it easy for us to build our page responsive. A responsive page automatically adapts to the screen of the end device on which it is viewed.
Bootstrap solves this mainly with its own grid system, which allows you to build a multi-column layout. The number of columns displayed side by side depends on the screen size. If the screen is too narrow and the grid is set up correctly, then a column automatically moves below another instead of being displayed next to it.
Create a Typo3 page (HTML Page Content) and add a Bootstrap grid with three columns containing any content. The columns should behave as follows:
3 columns should be displayed side by side on large screens (“lg”)
2 columns should be displayed side by side on medium screens (“md”)
1 column should take up the full width on small screens (“sm”)
Use the documentation for grids: https://getbootstrap.com/docs/3.4/css/#grid
Task 3 - Bootstrap Form
Create a form with Bootstrap styling that allows the creation of a new book. A book should have the same attributes as seen in HTML Task 1:
A book has a title (text field)
A book has an author – this should be selectable from a list of authors (dropdown)
A book belongs to one or more genres (checkboxes)
A book is either available for rental or already rented (radio buttons)
The Bootstrap documentation will be helpful: https://getbootstrap.com/docs/3.4/css/#forms
Note: The form does not need to do anything – when clicking submit, nothing has to happen.
JavaScript / jQuery
You will receive a short introduction to JavaScript and jQuery from your supervisor.
Task 1 - Show / Hide an Element with jQuery
Use JavaScript and (optionally) jQuery to extend the form from the previous task as follows:
The radio button “Available” should be set to “yes” by default.
When switched to “no”, three new input fields should appear below:
A dropdown with a person selection “Currently Rented by”.
A date field “Rental Start Date”.
A date field “Rental Due Date”.
When the radio button is switched back to “yes”, these three elements should be hidden again.
Note: For a date field, there are many possible solutions – native Bootstrap does not include one directly. Search the Internet for a datepicker solution that can be integrated! (e.g. “jQuery Datepicker”)
Basic Knowledge
Here only a small collection of important topices are explained.
See also Native Class: Extra. Read through it once and come back here.
General Tips
The Dev Panel will show latest STORE_RECORD values and others.
Debug a value: show value in the FormElement under Layout → Note field.
- Debug a query: Execute directly on the database. Tools like
https://www.phpmyadmin.net/ or mysql are useful.
Always have a sharp look at the Error / Exception - it helps a lot.
- In FormEditor, you’ll find the table definition of the primary always at the
last pill.
Define a strategy how to name forms:
E.g. person, person-admin, person… (all person forms listed together).
For subrecords, some might find it useful to prefix with sub.
Per sub-tool / sub-functionality, define prefixes and name the forms accordingly.
Add the columns modified and created to every table (Table structure). This helps debugging. Especially in the Glue table, this is useful to directly see whether a (new) entry was created, modified or untouched.
Pre-Render: In Report, links/buttons can be created at an early position, save the button in a variable via AS and hide it (AS ‘_link|newPersonBtn|_hide’). Afterwards, simply call the button at the place where necessary:
<th>{{&newPersonBtn:RE::-}}</th>.In Report work with code blocks
{}. Name them by simply writing a label before the opening bracket.Documentation: Sections Store, Form and Report are most important.
Under Report → QFQ CSS Classes, Bootstrap and Tablesorter find styling classes for tables in the report.
Under Form → Class: Native find explanation of the different FormElement types.
FormEditor: Form
Pill: Basic
Field: Name
The name under which the form is saved.
Field: Title
The headline of the form, visible to the user. It can be dynamically adjusted by logic – depending on the usage of the form.
{{SELECT IF( {{id:R0}} = 0, "Add a Genre", "Edit a Genre") }}
If a new form with ID 0 is created, Add a Genre will be displayed, otherwise Edit a Genre. The R stands for the Record Store. How it works and what it contains at what time can be found in the Store chapter.
Field: Note
Note for the developer in case they want to describe the form in more detail.
Field: Table
The table the form points to. The records can directly access the columns of this table without having to be selected first.
Pill: Form Element
This is where the elements that make up the form are created.
Pill: Layout
Field: Label Align
Choose to which side the content should align within its field. This field is defined by the columns.
Pill: Table Definition
This displays all columns from the table that was selected under Basic → Table.
FormEditor: FormElement
Pill: Basic
Field: Name
The name points directly to the table columns selected in the form.
Under Table Definition you can see the table with the column name/type/size/default.
Non-primary column names:
If a column from another table should be displayed, the name can be freely chosen as long as it does not match a column from the primary table (some more action is necessary to use read/save values).
Best practice is to start non-primary columns with my. Choose a self-explanatory name to make it’s use obvious.
Field: Label
This is the name you give the FormElement to distinguish it from others – especially if there are two or more pointing to the same column.
Field: Mode
The mode determines the behaviour. The names are self-explanatory.
Field: Mode SQL
Here the mode can be dynamically adjusted to the FormElement based on logic.
{{SELECT IF( '{{roleUser:UE}}' = 'course_role_admin', 'required', 'readonly' ) }}
In this example, it checks which role the user has and decides based on this role whether they may edit the content or not. The role comes here from the User Store.
Field: Class
Categorize FormElements:
Native: fields the user interacts directly with.
Actions: consist of logic executed in the background, e.g. validation so the same value cannot be selected twice. Be aware of FormElement.parameter.sql…. in native FormElements - they behave very similar to FE.class=action.
Container: are different sections in which you can group FormElements. They are explained in more detail in the tutorial.
Link to documentation Class: Container
Field: Type
Link to documentation Class: Native
Field: Value
The Value field is meant for outputting individual values – whether selected or hardcoded. This value can also be used in sql1, if it is e.g. an ID.
SELECT id AS _id, p.firstName FROM Person
The _id then takes the value that is in the Value field.
Field: sql1
This field is used for SQL queries and is mainly intended for when more than one value is returned. That is also why the ! is in front of the SELECT – it means the values will be returned as an array and not as a string.
Field: Parameter
In this field, properties are defined that every FormElement can have. For this you can look in the documentation of the individual Native classes. There the available parameters are always described.
Concept: Store
Link to documentation Store
STORE_RECORD
The Record Store is filled automically during form load with the given record. However, it needs an ID to know which person it should load. That’s why it is filled when editing a form, because the edit button passes the ID of e.g. a person. When leaving the page or reloading, the Record Store is overwritten with new data.
STORE_FORM
This store is filled with the inputs a user makes in a form in the moment when the user presses ‘save’. It is often used together with the Record Store. The Form Store is overwritten with latest user data on each ‘save’.
STORE_USER
This store is persistant during the user (=browser) session (limit: timeout or browser restart). Data remain in the store as long as the user is logged in and are overwritten when the user logs out.
Miscellaneous
Subrecord
If a FormElement Subrecord is created in a form, it establishes the connection between e.g. a specific person and the roles. In the Parameter field, the detail-form (=sub form) is referenced. E.g. all roles assigned to a person. In the detail form you then create a FormElement Select, where all roles, that a person can be assigned to, are offered to choose. This ensures that in the first step a specific person can be chosen and in the second step all roles are displayed.
Since we usually look at a linking table (Glue) in the subrecord, but must come from the Person table, we must specify this connection in the Parameter field.
Parameter
form = subSettingRole
detail = id:xId
subSettingRole is my second form with the Select. The ID comes from the Person table and the xId belongs to the Glue table. If I want to assign a role to a person, this happens by creating a Glue record. That’s why the first form selects Glue.id AS _id and the role Grp.name AS Role. Important: The first form only selects – it does not write.
The second form points to the Glue table, because this form writes an entry into the Glue table. The FormElement points to Glue.grId. Thus, a new Glue entry can be created:
ID auto generated,
xId from the parameter,
and the grId is now selected.
So in the sql1 field the gr.id (linked to the grId) and the role gr.name are selected.
Action
Link to documentation Class: Action
beforeSave Validator
In the FormElement under Basic, action is selected and the type beforeSave. Our code goes into the Parameter field and starts with sqlValidate = …. Then follows a normal SQL query where we fetch one value from the DB and one from the Form & Record Store.
It is also important here to specify the correct sanitize class (e.g. alnumx, digit, etc.). The alert is a pop-up with selectable content.
sqlValidate = {{!SELECT p.account
FROM Person p
WHERE p.account = '{{account:FRE:alnumx}}'
}}
expectRecords = 0
alert = Account name already exist
Further information: Class: Action.
Action in Typo3 Report
In my case, I created a button that opens a pop-up with an “Execute Yes/No” question. If you click Yes, the action is executed.
The button looked like this:
CONCAT('p:{{pageSlug:T}}?action=dropEnroll&enrollId=', e.id ,'|b|s|D|q:Do you realy want to drop the course?') AS '_link|studentCancelEnroll'
Action now contains dropEnroll and enrollId contains the e.id from the respective table row. Since the button stores these in the SIP Store, we first pass them into the Record Store.
{
sql = SELECT '{{enrollId:SE}}' AS _enrollId
,'{{action:SE::w}}' AS _action
}
Through AS, both are loaded from the SIP into the Record Store. Additionally, we wipe the SIP Store with the small w to avoid errors by using browser back/forth. Afterwards we can make changes with normal SQL statements that the button should perform.
In my example, that would be a status change.
parent {
sql = SELECT '' FROM DUAL WHERE '{{action:RE}}' = 'dropEnroll'
child {
sql = UPDATE Enrollment SET statusId = (SELECT grs.id
FROM (Grp grs)
WHERE grs.reference = 'course_status_dropped'
)
WHERE '{{enrollId:R0}}' = id
}
}
Since I query the action variable in the Dual table, I can execute multiple child blocks if desired. It can also be done – as here – in a single SQL statement:
{
sql = UPDATE Enrollment SET statusId = (SELECT gr.id
FROM Grp gr
WHERE gr.reference = 'course_status_dropped')
WHERE '{{enrollId:R0}}' = id
AND '{{action:RE}}' = 'dropEnroll'
}
Here the whole thing was solved via a subselect.
Native Class: Extra
I used this class to set IDs in a table. In a registration table, the user had filled out very little, the rest were all IDs from other tables.
Here I used the native class extra to connect these IDs via SELECT. It is a class that runs in the background together with the form.
Since I set an ID, I used the Value field to select it. Once I also just used an ID from a store.
QFQ
In this tutorial you will learn what QFQ is and how to use its many features. For each section there is an exercise to practice.
What is QFQ?
QFQ (Quick Form Query) is an extension for the Typo3 CMS. Its backend is written in PHP and its frontend in Javascript. First and foremost it allows the developer to connect their website to a custom database and display dynamic content. But displaying data is not everything QFQ is good at. QFQ stands for ‘Quick Form Query’ and at its core is an easy to use but powerful Form Editor. The Form Editor allows the developer to build custom forms that allow for user input, which can then easily be stored in the connected database.
So QFQ can display dynamic content and process user inputs. But this is not everything! QFQ comes with many more features, such as:
Sending emails (optional with attachment, maybe dynamically generated PDFs).
Execute a script (shell script, PHP, …) on the webserver and then process its output.
Execute SQL
Access to Typo3 Variables such as the currently logged in user, his/her user groups, etc.
and much more!
Report
When we talk about a Report in QFQ, what we really mean is a Typo3 page content of the type ‘QFQ Element’. In the most common and simple case, we just use Reports to take data from our database and show it to the user. However, reports are very powerful and can be used for anything you want on your website. The one thing reports are not meant for are forms to handle user input. Technically you could build your own form on a report, but QFQ comes with its own Form Editor feature and we highly recommend to use that instead.
Basic Report Syntax
Let’s start at the beginning. The first thing we want to do is to take data from our database and display it on our website. For this we are going to use - you guessed it - a report! Before we can start, we have to create a Typo3 page and add a page content of type ‘QFQ Element’ to it. When we edit this page content, we are greeted with a large empty box where we are supposed to write our QFQ code. QFQ code works by directly executing SQL queries, and using the result of these queries to render plain HTML. To do this, QFQ code has its own syntax. It’s really easy - start by looking at the following code:
{
sql = SELECT title FROM Book
}
{
sql = SELECT lastName FROM Person
}
The first thing to notice is that this code is divided into two blocks. Curly braces are used to define what’s inside these blocks.
You will also see 10 {} in older code, which stands for the order
of execution, where 10 {} executes before 20 {}
regardless of position. This is deprecated and no longer best practice and has been
replaced by top-down code execution.
Inside each block you see an SQL statement. SQL statements are the core of each
block, and since the whole report is made out of blocks, the SQL statements are
the most important part of a QFQ report.
The SQL is preceded by sql =, which is necessary because a block can consist
of more than just an sql statement. But more on that later.
What’s the result of this simple report? It is something like: The HobbitDonQuixoteTheDavinciCodeTolkiende CervantesBrown. First we selected the titles of all books - and they are automatically rendered as the output of the QFQ code. The same goes for the lastnames of all People.
The information in this form is of little use to a visitor of our website. What we need is formatting! So let’s add something to our first block:
{
sql = SELECT title FROM Book
head = <ul>
tail = </ul>
rbeg = <li>
rend = </li>
}
The SQL statement is still the same - but we have introduced 4 new keywords (you’ll find all keywords under QFQ Keywords (Bodytext)):
The head will be rendered before any of the sql. However, if the SQL returns nothing (0 rows), then no head will be rendered. If you want to render the head even if the SQL returns nothing, use shead instead (‘static head’).
The tail will be rendered after all of the sql. However, if the SQL returns nothing (0 rows), then no tail will be rendered. If you want to render the tail even if the SQL returns nothing, use stail instead (‘static tail’).
rbeg stands for ‘row begin’. rbeg will be rendered before every row of the sql result.
rend stands for ‘row end’. rend will be rendered after every row of the sql result.
And just like that, the output of our first block is now formatted HTML:
<ul>
<li>The Hobbit</li>
<li>Don Quixote</li>
<li>The Da Vinci Code</li>
</ul>
Let’s go a bit further for with the second block. We’ll change it to:
{
sql = SELECT firstName, lastName FROM Person
head = <table class="table">
<thead>
<tr>
<th>Firstname</th>
<th>Lastname</th>
</tr>
</thead>
<tbody>
tail = </tbody>
</table>
rbeg = <tr>
rend = </tr>
fbeg = <td>
fend = </td>
}
First of all, we are no longer using an unordered list to display our data here. We are now rendering an HTML table. We can use the head to create column headings too. Furthermore, in our SQL query we now select firstname and lastname. The result of this SQL query is a table with two columns. This means there are two fields per row now - a chance to build in some additional formatting:
fbeg can be used to render something before each field (but after rbeg).
fend can be used to render something after each field (but before rend).
And just like that we have changed the output of the second block to:
<table class="table">
<thead>
<tr>
<th>Firstname</th>
<th>Lastname</th>
</tr>
</thead>
<tbody>
<tr>
<td>J. R. R.</td>
<td>Tolkien</td>
</tr>
<tr>
<td>Miguel</td>
<td>de Cervantes<td>
</tr>
<tr>
<td>Dan</td>
<td>Brown</td>
</tr>
</tbody>
</table>
Note: There are even more QFQ body keywords such as rsep and renr! Please check QFQ Keywords (Bodytext).
Exercise 1
Create a new page ‘Person’ and add a QFQ content Element with the above QFQ code to it. (The code to create a table that contains all people)
Exercise 2
Add another column to that table which contains the email-address of the person
Exercise 3
Create another page, call it ‘Books’ and add a QFQ Element to it.
Display a table with all books on this page.
Add at least two columns - one for the title and one for the name of the author.
Nested Blocks and the Record Store
There are a few things going on in the following example:
parent {
sql = SELECT '<h1>', firstName, lastName, '</h1>', id AS _pId
FROM Person
child {
sql = SELECT title
FROM Book
WHERE authorId = {{pId:R}}
head = <ul>
tail = </ul>
rbeg = <li>
rend = </li>
}
}
We can see a parent block - parent { } - and a child block - child { }. The child block will be executed for each row that is returned by the parent block’s SQL statement. If we compare this with conventional procedural programming, we could say that the nested blocks create a for each loop.
But there is more going on in the example:
- You might have noticed that the ‘h1’ Element is directly part of the SQL
SELECT statement. The same could have been achieved by using the
rbegandrendattributes of the block. In fact, we advocate for the use of these attributes. Nevertheless, the possibility of including HTML directly in the SQL statements can be really useful sometimes!
In the parent block, we selected
id AS _pId. In plain old SQL, writingid AS pId(underscore is missing) would be used to set the column heading to ‘pId’. However, QFQ is doing more. By giving the id-field an alias (pId), QFQ automatically creates a variable named pId.This variable is put into the Record Store. There are several places where QFQ can store variables and the record store is arguably the most used. Other ‘Stores’ - places to store variables - will be discussed in later chapters.
By putting the underscore (AS _pId) in front of the alias, we can prevent QFQ from rendering this specific field. We don’t want random id-numbers appearing on our page, we just want to remember the id because we need it in the nested block.
In the nested block’s SQL statement we access the id we have stored before. We do this by typing
{{pId:R0}}. The double curly brackets are just the way to tell QFQ that we would like to access a variable. ‘pId’ is obviously the name of the variable we want. This is followed by ‘:R’, meaning that we fetch it from the Record Store. The zero at the end means that we replace this term with 0 if there is no variable ‘pId’.
The result of our QFQ code might look like this:
<h1>J. R. R. Tolkien</h1>
<ul>
<li>The Hobbit</li>
<li>The Lord of the Rings</li>{id:
</ul>
<h1>Miguel de Cervantes</h1>
<ul>
<li>Don Quixote</li>
</ul>
<h1>Dan Brown</h1>
<ul>
<li>The Da Vinci Code</li>
</ul>
Exercises
Create a new page and call it ‘Genres’. Add a QFQ Element to it. Write QFQ code that generates a heading (
@<h2>@) for each genre. Hint: We are storing genres in the tableGrp, but not all records in this table represent a genre. Only the records with@grId = [id of the Grp record with reference = 'library_genre']@are genres.Below each genre-heading, display a list (
@<ul>@) of all Books in that genre. Use a nested QFQ block and make use of the record store!
Client-Store, Zero and Empty
In the previous chapter it was briefly mentioned that the Record Store is not the only place QFQ can get data from. Introducing: The Client-Store! First we should explain how data is put into the client store. This is simple:
Existing HTTP POST variables are automatically put into the Client-Store.
This means that user data, which is submitted to the page via form, is in the Client-Store.
All parameters of the user’s requested URL are in the Client-Store.
In this case: https://www.mypage.com?id=100&customparam=abc we have 2 variables in the Client-Store:
idandcustomparam.
To access values from the Client-Store in a QFQ report, we can use:
{{variableName:C}}
However, you are more likely to be using it in one of these two ways:
{{integerName:C0}}
{{stringName:CE:alnumx}}
The first example is very similar to the one shown at the beginning. The difference is that we write:
{{integerName:C0}}
instead of {{integerName:C}} after the variable name. This should be used if you are sure that the variable contains
an integer. The 0 at the end simply means that the whole expression will be replaced by 0 instead of the variable’s
value, if the variable does not exist.
The second example,
{{stringName:CE:alnumx}}
should be used if the variable contains a string. Here’s what’s important:
Instead of
0, we appendEafter theCthat stands for “Client-Store”.Emeans “Empty” – the whole expression is omitted if the variable does not exist.There is another colon after
CE, followed byalnumx. This is the sanitize class. WithalnumxQFQ outputs only numbers, Latin letters and a few special characters. If the value does not match, an error occurs. See the official documentation.Any variable retrieved from the client store uses
digitas the default sanitize class. Therefore we must set a class explicitly for strings, but not for integers.If the variable is a string and you use it inside an SQL statement, add quotes around it (e.g.
'{{stringVariable:CE:alnumx}}'). Otherwise the resulting SQL is invalid.
Exercise 1
Create a page and call it
Book detail(add a QFQ content element).If the URL contains a parameter
bookId, then title, author and number of pages for the book with this id should be displayed.If there is no parameter
bookIdin the URL, then display the textNo book selected. Example URL: http://webwork20:<port>/home/bookdetailIf a
bookIdwas stated in the URL, but there is no book with this id in the DB, displayBook not found. Example URL: http://webwork20:<port>/home/bookdetail?bookId=111
Tips
The value (most likely HTML code) that is assigned to althead is displayed
when the SQL in the same block returns nothing:
{
sql = ...
althead = <p>Alt Element</p>
}
You can use the DUAL table and '' to use WHERE in SQL.
In SQL, DUAL is a special dummy table used to select a value or perform a
function when no actual table data is needed:
{
sql = SELECT ''
FROM DUAL
WHERE <Condition>
althead = <p>Alt Element</p>
}
In this example we now SELECT something if the condition is TRUE and if
FALSE we return the <p>Alt Element</p>.
On the page
Book, add a link to the Book Detail page for each book (add a new column or build a link directly around the book title).
Links and SIP
We have already seen two Stores. We used the Record Store within a report as our “short term memory”, saving an id to use it in another SQL query. We have also used the Client-Store to get the values of HTTP GET or POST variables.
There is another very important Store, the SIP-Store. Here’s what’s important:
The S in SIP stands for session. If you find a parameter
sin your URL, then its value is the session id. A session id in the URL might look like this:s=60fed01b63542The server will have one or more variables associated with a session, which is identified by the session id.
The SIP adds a great deal of security to our website, because SIP variables never leave the server!
Our website can be opened with an existing session id (passed via URL).
The server then decides how to display the requested page depending on the variables associated with the session. This can be done within a QFQ report!
Even if someone else knows our session id, they won’t be able to access the session! This is because every session is device-specific and access will be denied if tried from another device.
But how do we create a session in the first place? Take a look at the following QFQ code:
{
sql = SELECT 'p:/home?a=topSecret&b=3|s|t:Click Me' AS _link
}
There are quite a few things going on in this one line.
The whole string is selected with
AS _link. With your current knowledge you might expect that the string will be put in the Record Store under the namelink. However, this is not the case.QFQ has special column names, which are keywords that can completely change how a field is selected. Depending on the keyword, additional functionality might be triggered!
linkis one of those special column names and it is used to build links of various types. The kind of link that is built depends entirely on the string that is used. The string needs to follow a certain logic – see the official QFQ documentation.In our example, the string starts with
p:/home– meaning that the link will point to the page/home./homeis a so-called page slug, which is a unique identifier for the page and part of the URL.The page slug of any given Typo3 page can be viewed and edited in the Typo3 backend. If the link should not point to another page, but instead to the page the user is already on, you can use
p:{{pageSlug:T}}– this variable is automatically replaced by the slug of the current page from the Typo3 Store.After the page, the string is split by
?. This means that one or more parameters will follow. Additional parameters are separated by&.In our example, we assign the value
topSecretto a variable we callaand we assign3to the variableb.After all variables are set, there is another special character which further splits the string: the vertical bar (
|), also called “pipe”.In our example, we put an
sbehind the pipe, declaring that we want our variable to be SIP-encoded and not directly accessible in the URL.After another pipe, we then set the link-text to Click Me, using the character
t. The sequence does not matter in most cases:|s|t:Click Me=|t:Click Me|s.QFQ will now create a session id and associate the variables
aandbwith it. The actual output will be an HTML link:
<a href="https://www.mypage.com/home?s=badcaffee1234">Click Me</a>
As you can see, we now have a link which is using a session id – and this session
is only valid for our own device! Somewhere on the server, there is a note of this
session – and also of the variables we assigned to it earlier! The server knows
that the session with id badcaffee1234 has a variable called a with the
value topSecret and a variable b with the value 3.
We pointed our link to the Typo3 page /home, therefore we can make use of
our SIP variable on this page. Let’s add the following QFQ element there:
{
sql = SELECT IF('{{a:SE}}' != '',
'The variable a is {{a:SE}} and b={{b:S0}}',
'There is no session variable.')
}
If we open the page /home normally, we would see the text
There is no session variable.
However, if we open the page via the link we created earlier, then we will get:
The variable a is topSecret and b=3
pageSlug example
URL Before: https://project.math.uzh.ch/projects/praktikum
sql = SELECT 'p:{{pageSlug:T}}/tutorial?r=0|s|t:Click Me' AS _link
URL After: https://project.math.uzh.ch/projects/praktikum/tutorial?s=badcaffee1234
How SIP works
Exercise 1
Add a page
Person Detail. Then modify the pagePersonso that there is a SIP-link for each person to the page Person Detail. The SIP should contain the person’s id.Edit the page
Person Detailso that it checks for the person id in the SIP. If there is no id or the person with the id is not in the database, displayPerson not found. Otherwise display the person’s firstname, lastname and email address.
Integrating Forms on our Report
Here it all comes together. Let’s make a page on which we can see a table that lists all books. We also want an Edit button for each book, as well as a New Book button. Our goal is to link these buttons to forms we will create later on!
We are using the following QFQ code.
This time the code should explain itself, because we have added comments to it.
Comments in the right place can be invaluable. In QFQ, you can start a comment
with the # symbol. Inside SQL queries you can also use block comments.
If there is a form in the SIP, it will be displayed!
form = {{form:SE}}
# ?form=Book => clicking on the link opens the Book form
# &r=0 => the Book form is opened with record id = 0.
# This means a new book will be created instead of
# opening the form with data of an existing book
# |N => (NEW) Display a button with a plus-symbol instead of
# an unformatted link.
# If you want your link formatted as a button, but still
# choose your own text, you can use |b|t:Buttontext
#
# AS '*link|newBookButton|*hide' => link is a special column name and
# even with the underscore in front
# of it, it will be rendered.
# Unless we add '_hide' (another
# special column name)!
# To have a reference to the created
# link, we give it a custom name
# (newBookButton).
# Because we are using more than one
# name / special column name,
# we have to separate them with pipes
# (|) and put single ticks around the
# whole expression.
{
sql = SELECT 'p:{{pageSlug:T}}?form=Book&r=0|s|N' AS '*link|newBookButton|*hide'
}
Render table with all books. We insert the ``newBookButton`` in the table header
# &r=... => we set the record id to the book's id, so
# that our form will load the data of that
# specific book when opened
# {{&newBookButton:RE::-}} => The '&' means, that we want the computed
# value of the link, and not the original string!
# If we don't use the '&', we'll simply get
# the string 'p:/books&form=Book&r=0|s|b|N'
# => Notice the RE::-? After the store (RE)
# we could specify a 'Sanitize Class', which
# we don't need.
# This is why there is nothing between the
# two colons.
# After the next colon we can specify the
# escape/action class. The dash (-) means
# 'no escaping'.
# This is needed here, because
# '{{&newBookButton:RE}}' contains an
# HTML button and some of the characters are
# by default escaped by QFQ,
# resulting in invalid HTML if we don't
# tell QFQ not to escape it.
{
sql = SELECT CONCAT('p:{{pageSlug:T}}?form=Book&r=', b.id, '|s|b|E') AS _link,
b.title,
CONCAT(p.firstName, ' ', p.lastName)
FROM Book AS b, Person AS p
WHERE p.id = b.authorId
shead = <table class="table">
<thead>
<tr>
<th>{{&newBookButton:RE::-}}</th>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
stail = </tbody>
</table>
rbeg = <tr>
rend = </tr>
fbeg = <td>
fend = </td>
}
Finished page Code
form = {{form:SE}}
{
sql = SELECT 'p:{{pageSlug:T}}?form=Book&r=0|s|N' AS '*link|newBookButton|*hide'
}
{
sql = SELECT CONCAT('p:{{pageSlug:T}}?form=Book&r=', b.id, '|s|b|E') AS _link,
b.title,
CONCAT(p.firstName, ' ', p.lastName)
FROM Book AS b, Person AS p
WHERE p.id = b.authorId
shead = <table class="table">
<thead>
<tr>
<th>{{&newBookButton:RE::-}}</th>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
stail = </tbody>
</table>
rbeg = <tr>
rend = </tr>
fbeg = <td>
fend = </td>
}
Here’s what we see when we open our new book-page!
Since there are no books in our database, our table only has one row – the header!
By the way, if we hadn’t used shead and stail (and used head / tail instead)
we wouldn’t see anything at all.
The + button doesn’t do anything yet, because we have not created the Book form.
We will do that in the next chapter.
Exercises
Modify your existing Book page so that there is a button to create new books and a button for each book to edit (just like in the example above). You don’t need the Form yet, assume its name is
Book.Do the same for your Person page. Assume the Form name is
Person.
Forms
Let’s talk forms. Handling complex user input within a report would be entirely possible, but QFQ has another special solution for that – the Form Editor. The Form Editor is a Form that allows you to create forms – it’s Formception!
Before we can manage our forms, we need to set up the page on which we do that.
You can simply create a Typo3 page, call it Forms and add a QFQ Element to
it with the content:
file=_formEditor
In this case, we are loading the integrated QFQ code for the form management page
from a file, so we don’t have to code a custom solution for that.
You should also set the page slug for this Typo3 page to /form. You can do this
by editing the URL Segment in the General tab when editing the Typo3 page.
If you open your Forms page on the frontend, it should look like this:
For now you can ignore all the Json buttons – we will be working with the normal create/edit feature.
Note
Make sure your average user does not have permission to access the Forms page before your website goes live!
Creating a Form
Note that we are still using our Library-Database. The definitions of tables and columns are important when creating a form, because we will use that form to save data to these tables!
First we want to create a form on which we can edit Books for our library!
We create a new form by clicking the + button on the top left side of our
Forms page.
We are now in the Form Editor! There are several tabs (we also call them pills)
that will appear after we save, but for now we just fill out the Basic tab
and then hit the save button in the top right:
Form fields to fill in:
Name – Choose a name for your form, you will need it later to refer to the form. Since we will use the form to create books we will simply call it
Book.Title – The title is displayed at the top of the form (where the arrow is pointing in the screenshot above).
Note – You can take notes here – they will not be displayed to the user.
Table – Choose the table from your database for which you want to create/edit records. We choose the table
Book, because we want to create and edit books with our form! Think of this as the primary table of the form. It is still possible to show data from other tables, but anything from the chosen table will be much easier.
Exercise 1
Set up the Forms page if you haven’t already.
Create two forms: one called
Person(table: Person) and one calledBook(table: Book).
FormElements
Now that our form is created we can start adding FormElements to it. FormElements fall in one of three classes:
Native FormElements – the basic building blocks of your forms. They are either input elements such as textfields, checkboxes and dropdowns or elements to display data, like the note element.
Container FormElements – elements used to group together other form elements. You have already seen the tabs/pills in the Form Editor itself – these tabs are container FormElements.
Action FormElements – elements which are not displayed on the form, but they contain a trigger event and an action that is executed when triggered.
For our Book form we will stick to Native FormElements.
To add a FormElement, we switch to the Formelements tab and click the + button.
Another form opens, which allows us to define our first FormElement:
FormElement fields to fill in:
Name – In many cases, the name of your FormElement has to be exactly the name of a column on the primary table you chose for your form. In this example we are creating a textbox for book titles. Since we chose
Bookas our primary table andtitleis the column on that table, we simply choosetitle.If your FormElement does not represent a specific column on the chosen primary table, we suggest following a naming convention. We usually start these names with
my– e.g.myTitle.Label – The label that will be displayed next to our textbox.
Titlemakes total sense here, because the user should type a title.Class and Type – You can see the aforementioned FormElement class
nativehere. Because we want a simple textbox, we can leave both class and type at their default values.
Now we click the save icon on the top right side, and then the close icon right next to it. We are now back in our other form and we can see that our Form Element for the title was added to the list!
This form is still very basic, but let’s see if it works!
To do that we can open the Books page we have created in the previous chapter
and click the + button in the top left.
Our little form opens up! It looks like this:
Exercise 1
Recreate the Book Form with the
titleFormElement.Create a new form: * Name the form
Personand set its table toPerson. * Add two text inputs, one forfirstNameand one forlastName.
SELECT Statements and Variables inside Forms
Our form does not exactly behave the way we want: When we enter a title for a book and hit save, then we get a message that our book was saved. The new book also appears in our database, meaning it was really saved. But the form’s title still says Create New Book, even though the form contains
Exercise 1
Modify the title of your Book form so that it displays either Create or Edit (as seen in this chapter).
Do the same for the Person form.
Dropdown and sql1
We want to add a dropdown to select the author of the book from a list of names.
The HTML tag for a dropdown is select and QFQ also calls them that.
Let’s add another FormElement to the Book form and fill out the Basic tab:
So far we told QFQ to render a dropdown, but not what values should be available to select. We can configure that in the Value tab:
The above screenshot shows a simple way of filling our dropdown with values directly from the database:
We use the sql1 text area.
Since we are writing an SQL statement, we have to put everything in double curly brackets
{{ ... }}.This specific way of filling the dropdown requires the data to be in an array format.
We achieve this by putting an exclamation mark at the beginning:
{{! ... }}.
We are selecting the person’s id as
idand the person’s full name aslabel.The id is the important part that is needed for correct referencing of the selected person.
The label is what will be displayed – it’s the text of the option.
This is the explicit way of doing it. If you don’t write
AS idandAS label, then QFQ will implicitly take the first column as the id and the second as the label.
Id as Id example for a list of people
For every person in the table
Peoplea new dataset is created:{id:'name lastName'}.These datasets are then used to fill the select element so that when selected we know to which ID the name and last name belong.
Exercise 1
Add the author dropdown as shown in this chapter to your Book form.
FormElements like the dropdown (
select) have more customization options! Check the official documentation to see what’s possible: http://docs.qfq.io/master/Form.html#type-select Use a parameter (find it in the documentation) on the Author dropdown to add an “empty” option.Create a new form, call it
Rental. This form should contain: * A dropdown to select the book that was rented * A dropdown to select who rented the book * A date FormElement for the date it was rented * A date FormElement for the due date for returning the book * A datetime FormElement to store when it was actually returned
Check & Order
Now we have two FormElements. They are displayed in the same order as we have added them. But what if we want to display the author dropdown first and the title textfield second? In the list of FormElements you can simply drag and drop FormElements to change their order.
This automatically adjusts the Order field of the FormElements. We can also set a value for Order manually in the Check & Order tab of the FormElement.
Apart from ordering, this tab is all about data validation.
With the field Check Type we can define what kind of input is allowed for the
FormElement and what’s not.
For example you might have an input text field for an email. Simply select the
Check Type email and you are done!
QFQ will stop the user from submitting the form, if the user hasn’t entered a
valid email.
For our Book form, we are adding a FormElement of type text where the user
can enter the number of pages the book has.
What we need here is Check Type digit to allow integers only!
But actually we don’t have to bother, because the Check Type is on auto by
default and QFQ noticed that our nrOfPages are stored as an integer in the
database.
That’s why auto mode chooses digit for us and QFQ even adjusts the
text input element to optimize it for numbers:
So you probably won’t have to come to the Check & Order tab very often,
because QFQ is auto-checking and the ordering is easier to do by dragging and
dropping.
But in cases like email it is very useful. In a more complex scenario you can
choose Check Type pattern and write your own regular expression.
Exercise 1
Add the Number of Pages FormElement you have seen in this chapter to your Book form.
Add a FormElement to the Person form where the person’s email address can be entered. The field should only accept valid email addresses.
Layout
QFQ is delivered together with Bootstrap! That’s why we can use Bootstrap classes anywhere on our website and also why QFQ forms already have that Bootstrap look.
By default, our FormElements are placed inside a row which takes up the full width of the form. Furthermore, each FormElement is divided into three areas:
Label – Displays the text or HTML from the Label field in the Basic tab of the Form Editor.
Input – Displays the chosen input element from the Type field in the Basic tab of the Form Editor (e.g. a textbox, dropdown, etc.).
Note – Displays the text or HTML from the Note field in the Layout tab of the Form Editor. (You can see this field on the next screenshot)
Each of these areas has a width assigned to them. The default is 3 units for the label, 6 for the input and 3 for the note – making a total of 12! As you might know, a Bootstrap (BS) row is 12 units wide. You can change the individual widths of the 3 areas in the Layout tab of the Form Editor.
Suppose you wanted to display the label above the input. To achieve this you can
simply give the label a width of 12 units – making a full row and forcing the
following elements below the label.
Specifically, you would type 12 into the field BS Label Columns.
You can also use the full Bootstrap classes with screen size conditions like
col-lg-6 col-md-12.
In the screenshot above you see the whole Layout tab, so let’s go through some simple but useful options we have there:
Label / Input / Note – with these checkboxes you have full control over the HTML tags that will be rendered for the three areas. You could even uncheck
/rowfor a FormElement and uncheckrowfor the following FormElement. In the resulting HTML both FormElements will share the same row. This then allows you to place them next to each other!Maxlength – The maximum length of characters which the user may enter. This is only relevant for text inputs.
Note – Text or HTML to display in the Note area, which is to the right of the input by default.
Tooltip – A text to be displayed when the user hovers over the Form Element.
Placeholder – The placeholder text will be displayed if the user has not typed anything yet. This is only relevant for text inputs.
Keep in mind that you can use {{qfqVariables:RE}} and
{{SELECT 'Statements'}} in these fields within the Form Editor.
Especially in the Note field this can be very useful.
Exercise 1
The Number of Pages input field looks too wide. Make it smaller!
Modify the firstname and lastname FormElements on the Person form so that they only take one line and have placeholders. It should look like this:
Also acceptable:
Value
Let’s talk about the Value tab in the FormElement Editor. In total there are three fields in the Value tab:
value This field can be used to specify the default value of the FormElement when the form is opened. If a form is opened with an existing record (
r > 0) then the value of the FormElements is loaded from the corresponding column (as configured in the FormElement’s name) in the database. This means that if we want a default value, we will often have to write a statement like this:{{SELECT IF( {{id:R0}} = 0, 'New Book', '{{title:RE}}' )}}
This expression will fill a text field with New Book if a new book is being created, but keep the existing book’s title if the form is opened to edit an existing record.
sql1 If a FormElement requires more data, this field can often be used to define that data in the form of an SQL
SELECTstatement. We have already used this to generate all the options when we were creating a dropdown.parameter FormElements can often be configured beyond their default settings with this field. The parameters that can be used depend on the type of the FormElement. You can look up valid parameters for a specific FormElement in the official documentation.
Exercises
Set the default value of the title field on the Book form to New Book.
Set the default value of the number of pages field on the Book form to
100.
Subrecord
Let’s make use of the Value tab now and add a subrecord! What’s a subrecord? A subrecord, by our own definition, is a table within a form that contains multiple elements belonging to the primary record displayed in the form. These elements often have their own forms where they can be edited. In our subrecord we can display links to those forms to quickly edit the sub-elements.
We would like to add a subrecord to our Book form that lists every rental of the book (the person who has rented the book, start date, due date to return the book and the date when the book was returned). The following screenshot shows what our finished subrecord will look like:
First we create a few records in our database in the table Rental – otherwise our list of rentals would be empty. On our Book form we then create a new FormElement of the class native and the type subrecord. Look at the screenshots below to see how we configure the subrecord Form Element:
sql1 Code
SELECT r.id AS '_id',
CONCAT(p.firstName, ' ', p.lastName) AS 'Person',
DATE_FORMAT(r.start, '%d.%m.%Y') AS 'From',
DATE_FORMAT(r.end, '%d.%m.%Y') AS 'Due',
DATE_FORMAT(r.returned, '%d.%m.%Y') AS 'Returned'
FROM Rental AS r, Person AS p
WHERE r.bookId = {{id:R0}}
AND r.personId = p.id
Notes:
In the first screenshot, notice that there is an additional option at the bottom called Subrecord Option. By checking all the boxes (new, edit, delete) we will automatically get buttons to create, edit and delete our rental records.
Notice how we chose
myRentalas the name of the FormElement. - The rentals are not stored in the same record as the book, so we cannot hoose a column name from our primary table (Book). - We prepend my to the name as a best practice, so that any developer can quickly see that the data in this FormElement is not from the primary table.In the second screenshot, you can see that we can use the sql1 field to specify what data will be displayed in the subrecord. - By choosing custom column names we can set the column headers of the subrecord. - We need to include the id in our SELECT statement so that QFQ can build valid new/edit/delete-links. - To display only rentals of the book that is currently opened in the form, it is important that we use the condition:
WHERE r.bookId = {{id:R0}}
In the parameter field we wrote:
form=Rental
All of the buttons on this subrecord (new, edit, delete) are now linked to a form with the name Rental. For example, if we click the edit button on a rental record, the Rental form should open up and load the data of the chosen rental record.
Something else you will most likely want to add to the parameter field is:
detail=id:bookId
This will pass the id of the subrecord’s form (in this case the Book form) to the form that is linked to the subrecord (in this case the Rental form). Not only is the id passed on, but it is mapped to the field
bookIdon the new form. This means that the Rental form will have the correct book preselected when opened via our subrecord!
Exercise 1
Create the subrecord for rentals on your Book form as seen in this chapter.
Create another subrecord for rentals on your Person form. This time the subrecord should list all the books the person has rented.
Create another subrecord on the Person form, call it authoredBooks. It should show all the books the person has written.
Container FormElements
Our subrecord for the rentals seems a bit out of place where we are displaying it right now. It does not belong right next to very basic information about a book, like its title or author. Let’s divide the form into two tabs – one for general information and the second for rentals.
To do this, we create two FormElements, both of class container and type pill:
Now we want to place all of our other FormElements in one of the pills. Let’s rearrange the order of our FormElements, so that the pills are above the elements they should contain:
All the other FormElements are now displayed with a red background, because these elements are not yet assigned to a container. In the Basic tab of all these FormElements, we can now assign them to the pills:
Once we have done that for all of our FormElements, here’s the result:
Pills are the container we use the most. But there are also two other container types:
fieldset – can be used to further group elements within a pill (a normal HTML fieldset).
template group – is used to group a few elements together and repeat them x times. The user can create a new set of these elements by clicking a
+button and remove any entry by clicking the-button next to it. Template groups are similar to subrecords since they are used to manage sub-records of the primary record on a form.
Exercise 1
Create the General pill and the Rental pill on your Book form and assign all FormElements to their pill.
Create a General, Rental and Authored Books pill on the Person form. Assign the subrecords to their corresponding pill and the rest of the Form Elements to the General pill.
Dynamic Update and the Form-Store
In a previous exercise, we made this form to create and edit Rental records.
Let’s make a little quality of life upgrade for the librarian. Suppose books can be rented only for one month at a time. If a librarian manually creates a new rental, he or she will always have to type both start and due date.
To make life easier, we could automatically fill in the due date when the start date is entered. But how can we change the value of the due date input whenever the start date input changes?
Without QFQ, we would most likely solve this with a few lines of JavaScript. In QFQ it is even simpler than that: what we are looking for is called Dynamic Update.
Dynamic Update is a setting that can be enabled or disabled on any FormElement. By default it is disabled, but we need to enable it on the start FormElement and on the due FormElement. Simply check the box in the Basic tab of the FormElement:
What does it do though? If the value of any FormElement with Dynamic Update changes, then all FormElements with Dynamic Update will be refreshed. Refreshed in this case means: all of the fields you can configure for the FormElement are recalculated.
Since we want to automatically set the value of the due date, let’s add this to the value field of our due date element:
{{
SELECT IF('{{end:RE}}' = '',
DATE_FORMAT(DATE_ADD('{{start:RE}}', INTERVAL 1 MONTH), '%Y-%m-%d'),
'{{end:RE}}')
}}
In this query, we check if there is already a due date entered.
If there is one, we keep it – otherwise our code might accidentally overwrite a date the user has set manually.
If there is no due date yet, we set it to start date + 1 month. In MySQL this is done with DATE_ADD and INTERVAL.
We also have to use DATE_FORMAT so that the output matches the expected formatting of the date field.
However, recalculating the fields has no visible effect yet, because the result is still the same. We are missing one piece of the puzzle. Compare the above code to this one:
{{
SELECT IF('{{end:FRE:alnumx}}' = '',
DATE_FORMAT(DATE_ADD('{{start:FRE:alnumx}}', INTERVAL 1 MONTH), '%Y-%m-%d'),
'{{end:FRE:alnumx}}')
}}
The difference is that we are now also using the Form Store.
The Record Store within a form only contains values that are already saved in the database. Record Store variables therefore only change after saving again – that’s the issue here.
The Form Store, however, contains the values of all FormElements on the form. These values are updated immediately when a Dynamic Update happens!
Just like the Client Store, the Form Store by default tries to cast your variable into a digit, which is why we use the sanitize class
alnumx.When we specify
FREas the store of our variable, QFQ will first try to get it from the Form Store, then from the Record Store, and if neither exists, it returns an empty string.
Exercise 1
Modify the Author dropdown on your Book form. Use the Note field of the FormElement (Layout tab) to display all other books this author has written (e.g. Author of: The Hobbit, …). This text must update dynamically when another author is selected from the dropdown!
In the Rental form, add a FormElement of type note that says Overdue!. This info should only be displayed if: - the book has not been returned yet (
returned = 0), and - the current date is past the due date.Use the mode sql field in the Basic tab of your FormElement to show or hide it! - If your SQL returns
show, the element will appear. - If it returnshidden, it will not.Furthermore, this info must update dynamically as the user edits the form values.
Action FormElements
There is a third class of FormElements we haven’t talked about: Action FormElements. These elements execute an action when a specified event happens. Most of them already have their trigger in the name:
beforeLoad / afterLoad – execute before/after the form loads
beforeSave / afterSave – execute before/after the form is saved (both on INSERT and UPDATE)
beforeInsert / afterInsert – execute before/after the form saves a new record (
r=0→ INSERT)beforeUpdate / afterUpdate – execute before/after the form updates an existing record (
r!=0→ UPDATE)beforeDelete / afterDelete – execute before/after the record in the form is deleted
But how do we specify what exactly happens when one of these events is triggered?
Example: When a Book is deleted from the database, Rental records may remain that still reference this book. We want to add a beforeDelete action to the Book form that deletes all Rental records linked to the deleted book.
For this we can use parameters that contain the actual action. The simplest one is sqlAfter.
The name is confusing because it says “after”. But since we already defined that the action belongs to beforeDelete,
QFQ knows when to execute it.
If there are multiple actions of the same class (e.g. multiple beforeDelete elements), sqlBefore runs before
sqlAfter.
If you use only sqlAfter, then execution order is determined solely by the Order field of the Action Elements.
The sql in sqlAfter means that we are going to execute SQL when the action is triggered. Here is the configuration of our beforeDelete element:
Now, if we delete a book, all Rental records referencing this book are also deleted.
Alternative solutions:
Configure CASCADE rules in the MySQL database. This requires proper foreign keys.
Instead of deleting, many applications just mark a record as “deleted” (soft delete). That way no references are broken, since the data still exists.
Exercise 1
Add the beforeDelete element from this chapter to your Book form.
Add another beforeDelete element to the Person form that deletes all Rentals of this person.
On the Book form, it is currently possible to enter a negative number of pages. Use an Action FormElement to validate user input when the form is saved and display an error message if the number is invalid.
Instead of one parameter (sqlAfter), you will need three in this case: sqlValidate, expectRecords, messageFail.
Check the documentation to see exactly how an Action FormElement can be used this way:
http://docs.qfq.io/master/Form.html?#formelement-parameter-sqlvalidate
Tip: sqlValidate, expectRecords and messageFail all go into the Form.Parameter.
You might need to use {{! ... }} for the sqlValidate parameter.
Example:
sqlValidate = <QUERY> expectRecords = <NUMBER> messageFail = "FAIL"
Using Report-Syntax inside a Form
There are (very few) cases, in which normal QFQ FormElements are not able to achieve exactly what we want. Luckily, we have the option to write in QFQ-Report Syntax within a Form! This is a hidden, but very useful feature - because anything is possible in a Report!
No actual use case is shown here, because these are often the most complicated of cases and they have no place in this tutorial. We are just going to show how it is done. On our Book Form in the rental tab we will add a FormElement with type note. Usually it is used to display a small text or a little bit of HTML. We configured it like this:
Name: reportAvailable
Type: note
BS Label Columns: 0
BS Input Columns: 12
BS Note Columns: 0
Value:
The result is the following label, which states whether the book is currently available or not:
The takeaway from this is: You can use the power of the Report syntax in a Form by starting with #!report. However, you should only use this when needed, because things get messy when this is overused!
Exercise 1
Add a Note FormElement to any of your forms and use it to write Report Syntax. It doesn’t matter what your report does - go wild!
Bonus Exercises
Solution
The solutions for Bonus & Advanced are both under Advanced. Depending on QFQ version there might be slight differences in style.
Typo3 Backend: https://webwork22.math.uzh.ch/qfqtutorial/typo3 | Frontend: https://webwork22.math.uzh.ch/qfqtutorial
Exercise 1
When we create a new ‘Rental’ via subrecord on the Book-Form, then we would like the Book to be preselected on the Rental-Form that opens.
Study the ‘FormElement.parameter’ section of http://docs.qfq.io/master/Form.html#type-subrecord and find out how we can pass the book’s id from the Book Form to the Rental Form
Pass the person’s id instead, if the Rental Form is opened via subrecord on the Person Form.
Create a Form BookGenre. The Form should have a ‘readonly’ (FormElement.mode) dropdown that has a preselected Book and a dropdown that contains a list of all Genres.
Create a ‘Genres’ subrecord on the Book Form. When a Genre is added, the correct Book needs to be preselected on the BookGenre Form!
Create a beforeSave action FormElement on the BookGenre Form that stops the user from adding the same genre to a book twice!
QFQ beforeSave Doc: http://docs.qfq.io/master/Form.html#formelement-type-before-after
Create a Page “Rentals”.
Create a Summary section at the top of this page. Show the following statistics:
Number of books rented (all time)
Number of books currently rented
Number of books overdue and not returned
Top 3 most rented books
Top 3 people who rented most books
Below the Summary, create a table with all Rentals. Show person name, book title, start, due and returned dates.
The whole row should have a green/success background color, if the book has already been returned. If the book is overdue, the row’s backgroundcolor should be red.
Bootstrap Doc: https://getbootstrap.com/docs/3.4/css/#tables-contextual-classes
Depending on the approach, you might also need _noWrap: http://docs.qfq.io/master/Report.html#special-column-names
Order the whole table so that the Rentals with the earliest due dates appear at the top.
Make a button above the table that allows you to hide all Rentals that have already been returned. If the user is currently hiding these records, the button should say ‘Show returned Rentals’ instead.
Create a Page “Settings” in the Admin-area. Create a report on this page that shows a table with all Grp-records with
grId=0(Show Edit-Button, Name, Reference and number of sub-elements). Also create a ‘new’ button.
Create a Form to edit Grp-records (grId, name, reference, subrecord for all child-Grps).
Add an upload Form-Element to your Book Form, where the cover-image of the book can be uploaded.
Study the upload element in the official documentation (Upload Simple mode): http://docs.qfq.io/master/Form.html#type-upload
You will be saving the image path in the column coverPathFileName of the Book Table in your database. The image itself will be saved somewhere in the ‘fileadmin’ folder.
Display the uploaded cover image in two places:
On the Book-page as a small thumbnail (50px wide): http://docs.qfq.io/master/Report.html#column-thumbnail
In the Book-Form below the upload-element (no size restrictions):
Advanced
These things are not part of the tutorial, but are good to know!
System-Store (Y), QFQ-Config
More special columns - sendmail (mit ‘action-Pattern’). Might not work depending on Docker configurations - pagee, pagen and paged http://docs.qfq.io/master/Report.html#columns-page-x - thumbnail http://docs.qfq.io/master/Report.html#column-thumbnail
Functions
T3 feUser/feUsergroups, custom Person and Person_Role tables linked via ‘account’ column - sync Person with T3 feUser/feUsergroups - LDAP connection
User Store, Switch User
More FormElements http://docs.qfq.io/master/Form.html#class-native - Template Groups - Upload - Sendmail http://docs.qfq.io/master/Form.html#type-sendmail Might not work depending on Docker configurations - Checkbox & Checkboxlist - Radio & RadioList - Editor - Typeahead - Tag-FE
Tablesorter
Small Project
Requirements
You can choose a generic example, e.g. students with lectures. A task description should then be formulated that requires the following features:
Create / Edit / Delete records
Subrecord or Template Group
File Upload (pdf)
Email sending
Download (pdf)
Concept
Create a mockup, e.g. with draw.io, balsamiq or by hand…
Create a database schema
Implementation
The learner should implement the self-created concept as independently as possible after consultation with the supervisor. For specific problems (e.g. file upload, subrecord, email sending …) links to the relevant sections in the QFQ documentation will be provided.
Testing
Test cases will be derived from the requirements. A test protocol should be created and afterwards the tests should be carried out.
Typo3 Basic Setup
Install Extensions QFQ and Typo3: - https://qfq.io/download/?dir=snapshots (Take actual master or develop version) - https://www.math.uzh.ch/repo/?dir=uzhcd/UZH_CD_2023 (Take actual version)
Create a new Home page and define UZH CD template:
Create this qfq tt-content in your Home page:
# Link for qfq tutorial
# Title
{
sql = SELECT '<h2>QFQ Dev Environment</h2>'
}
# Todo info
{
sql = SELECT '- Execute the exercises from the tutorial links below in your qfq dev environment'
, 'Feel free to create your own new subpages and qfq reports on backend side: <a class="btn btn-default" href="{{baseUrl:Y}}typo3" target="_blank">T3 Backend</a>'
, 'You need new tables/columns or changes in your database? Here we are: <a class="btn btn-default" href="http://webwork20:<db-port>" target="_blank">Database</a>'
, 'QFQ Framework Documentation: <a class="btn btn-default" href="http://docs.qfq.io/master/" target="_blank">QFQ Doc</a>'
head = <h3>Todo:</h3>
fend = <br>
}
# Tutorial link info
{
sql = SELECT '<a class="btn btn-default" href="https://project.math.uzh.ch/projects/praktikum/wiki/QFQ_Introduction#Vorkenntnisse-SQL" target="_blank">SQL Basics</a>'
, '<a class="btn btn-default" href="https://project.math.uzh.ch/projects/praktikum/wiki/QFQ_Introduction#Vorkenntnisse-Web-Entwicklung" target="_blank">HTML/CSS/JS Basics</a>'
, '<a class="btn btn-default" href="https://project.math.uzh.ch/projects/praktikum/wiki/QFQ_Introduction#QFQ" target="_blank">QFQ</a>'
head = <h3>Tutorial Links:</h3>
fbeg = <br>
}
# Credentials and url info
{
sql = SELECT 'QFQ_DEV_INFO.txt'
head = <h3>Hints:</h3>Your initial credentials and the urls for frontend/backend/database can be found in file
}