` opening tag:
```xml
```
- In the {file}`rating` folder, add a new file named {file}`rules.xml` containing:
```xml
```
Let's detail what it does:
- the `after` rule targets the page heading
(identified by the `.documentFirstHeading` selector),
but it only applies when we are viewing a talk
(`.template-view.portaltype-talk`),
- the `include` rule retrieves the Rapido block content.
````
```{note}
This presumes having completed {doc}`dexterity`.
```
Now, if you visit a talk page, you see the counter below the heading.
```{image} _static/rapido-static_vote_block.png
:align: center
```
### Make Our Blocks Dynamic
```{only} presentation
- We can include dynamic **elements** in our block layout.
- Elements will be declared in the YAML file.
- They will computed using code provided in the Python file.
```
```{only} not presentation
The YAML file allows us to declare elements.
The Python files allows computing the *element value* using a function named after the element id.
And the HTML file can display elements using the curly-brackets notation.
The 3 files must have the same name (only the extensions change).
As mentioned earlier, the **Python file** contains the application logic.
This file is a set of Python functions named to correspond to the elements or the events they relate to.
For a `BASIC` element for instance,
if we provide a function with the same name as the element,
its return-value will be inserted in the block at the location of the element.
For an `ACTION`,
if we provide a function with the same name as the element,
it will be executed when a user clicks on the action button.
```
A typical element is defined and used as follows:
- create a definition in the YAML file:
```yaml
elements:
answer:
type: BASIC
```
- create an implementation in the Python file:
```python
def answer(context):
return 42
```
- insert the element in the HTML template:
```html
Answer to the Ultimate Question of Life, the Universe, and Everything: {answer}
```
#### Exercise 3: Create An Element To Display The Votes
Let's replace the "`0`" value in our rate block with a computed value.
To do this, you need to add an element to the block.
For now the Python function will return `10`.
````{dropdown} Solution
:animate: fade-in-slide-down
:icon: question
- In the {file}`blocks` folder, add a new file named {file}`rate.yaml` containing:
```yaml
elements:
display_votes:
type: BASIC
```
- Add also a file named {file}`rate.py` containing:
```python
def display_votes(context):
return 10
```
- And change the existing {file}`rate.html` as follows:
```html
{display_votes} votes
```
````
Now, if you refresh your talk page, the counter will display the value returned by your Python function.
```{image} _static/rapido-votes_from_python.png
:align: center
```
### Create Actions
An {dfn}`action` is a regular element, but it is rendered as a button.
Its associated function in the Python file will be called when the user clicks on the button.
Example:
- YAML:
```yaml
elements:
change_page_title:
type: ACTION
label: Change the title
```
- Python:
```python
def change_page_title(context):
context.content.title = "A new title"
```
- HTML:
```html
{change_page_title}
```
Every time the user clicks the action, the block is reloaded (so elements are refreshed).
When the block is inserted in a Plone page using a Diazo rule,
the reloading will replace the current page with the bare block.
This is not what we want.
If we want the current Plone page to be preserved, we need to activate the AJAX mode in the YAML file:
```yaml
target: ajax
```
#### Exercise 4: Add The Like Button
Add a {guilabel}`Like` button to the block.
For now, the action itself will do nothing.
Let's just insert it at the right place, and make sure the block is refreshed properly when we click.
````{dropdown} Solution
:animate: fade-in-slide-down
:icon: question
- in {file}`rate.yaml`, add a new `like` element and change the target to `ajax`
After doing this, your YAML file looks as follows:
```yaml
target: ajax
elements:
display_votes:
type: BASIC
like:
type: ACTION
label: Like
```
- in {file}`rate.py`, add a new function:
```python
def like(context):
# do nothing for now
pass
```
- and in {file}`rate.html`:
```html
{like} {display_votes} votes
```
````
```{image} _static/rapido-static_like_button.png
:align: center
```
### Store Data
Each Rapido app provides an internal storage utility able to store records.
Records are not Plone objects, they are just simple dictionaries of basic data (strings, numbers, dates, etc.).
There is no constraint on the dictionary items but Rapido will always set an `id` item, so this key is reserved.
Something like:
```python
{'id': 'record_1', 'name': 'Eric', 'age': 42}
```
could be a valid record.
The Rapido Python API allows us to create, get or delete records:
```python
record = context.app.create_record(id="my-record")
record = context.app.get_record("other-record")
context.app.delete_record("other-record")
```
The record items are managed like regular Python dictionary items:
```python
record.get('age', 0)
'age' in record
record['age'] = 42
del record['age']
```
#### Exercise 5: Count Votes
The button is OK now, now let's focus on counting votes.
To count the votes on a talk, you need store some information:
- an identifier for the talk (we will use the talk path, from the Plone `absolute_url_path()` method),
- the total votes it gets.
Let's implement the {py:func}`like` function:
- first we need to get the current talk: the Rapido {py:obj}`context` allows to get the current Plone content using {py:attr}`context.content`,
- then we need to get the record corresponding to the current talk,
\- if it does not exist, we need to create it,
- and then we need to increase the current total votes for that talk by 1.
````{dropdown} Solution
:animate: fade-in-slide-down
:icon: question
```python
def like(context):
current_talk = context.content
talk_path = current_talk.absolute_url_path()
record = context.app.get_record(talk_path)
if not record:
record = context.app.create_record(id=talk_path)
record['total'] = 0
record['total'] += 1
```
````
```{only} not presentation
Note: we cannot just use the content `id` attribute as a valid identifier
because it is not unique at site level, so we prefer the path.
```
Now let's make sure to display the proper total in the `display_votes` element:
- here also, we need to get the current talk,
- then we get the corresponding record,
- and we get its current total votes.
```python
def display_votes(context):
talk_path = context.content.absolute_url_path()
record = context.app.get_record(talk_path)
if not record:
return 0
return record['total']
```
```{image} _static/rapido-votes_updating.png
:align: center
```
### HTML Templating Vs TAL Templating
#### HTML templating
The Rapido HTML templating is very simple.
It is just plain HTML with curly-bracket notations to insert elements:
```html
This is my message: {message}
```
If the element is an object, we can render its properties:
```python
def doc(context):
return context.content
```
```html
This is my title: {doc.title}
```
And if the element is a dictionary, we can access its items:
```python
def stats(context):
return {'avg': 10, 'total': 120}
```
```html
Average: {stats[avg]}
```
It is easy to use but it cannot perform loops or conditional insertion.
#### TAL Templating
TAL templating is the templating format used in the core of Plone.
If HTML templating is too limiting, Rapido allows you to use TAL instead.
We just need to provide a file with the {file}`.pt` extension instead of the HTML file.
The block elements are available in the {py:obj}`elements` object:
```python
def my_title(context):
return "Chapter 1"
```
```html
```
Elements can be used as conditions:
```python
def is_footer(context):
return True
```
```html
```
If an element returns an iterable object (list, dictionary), we can make a loop:
```python
def links(context):
return [
{'url': 'https://validator.w3.org/', 'title': 'Markup Validation Service'},
{'url': 'https://www.w3.org/Style/CSS/', 'title': 'CSS'},
]
```
```html
```
The current Rapido context is available in the {py:obj}`context` object:
```html
```
See the [TAL commands documentation](http://www.owlfish.com/software/simpleTAL/tal-guide.html) for more details about TAL.
### Create Custom Views
For now, we have just added small chunks of HTML in existing pages.
But Rapido also allows you to create a whole new page (a Plone developer would call it a new **view**).
Let's imagine we want to display one of our Rapido blocks in the main content area instead of the regular content.
We *could* do it with a simple `replace` Diazo rule:
```xml
```
But if we do that, the regular content will not be accessible anymore.
What if we want to be able to access both the regular content with its regular URL,
and define an additional URL to display our block as main content?
To accomplish this, Rapido allows you to declare **neutral views**.
By adding {samp}`@@rapido/view/{}` to a content URL we get the content's default view.
The `any-name` value can actually be **anything**, we do not really care,
we just use it to match a Diazo rule in charge of replacing the default content with our block:
```xml
```
Now if we visit for instance:
```
http://localhost:8080/Plone/page1/@@rapido/view/show-stats
```
we see our block instead of the regular page content.
(And if we visit , we get the regular content of course.)
#### Exercise 5: Create The Top 5 Page
Let's create a block to display the Talks Top 5:
- It needs to be a specific view.
- We will use a TAL template (but for now the content will be fake and static).
- Visitors will access it from a footer link.
`````{dropdown} Solution
:animate: fade-in-slide-down
:icon: question
First we create a {file}`top5.pt` file in the {file}`blocks` folder with the following content:
```html
Talks Top 5
```
Now we add the following to our {file}`rules.xml` file:
```xml
```
And then we declare a new action in our footer:
- go to the `site_actions` in the Zope Management Interface:
```
http://localhost:8080/Plone/portal_actions/site_actions/manage_workspace
```
- add a new `top5` action, with the {guilabel}`URL (Expression)` property set to:
```
string:${globals_view/navigationRootUrl}/@@rapido/view/talks-top-5
```
````{versionadded} 5.1
1. go to {menuselection}`Site Setup --> Actions`
2. add a new action in Site actions category with name "Top 5" and as URL:
```
string:${globals_view/navigationRootUrl}/@@rapido/view/talks-top-5
```
````
`````
```{image} _static/rapido-added_cmf_action.png
:align: center
```
### Index And Query Records
Rapido record items can be indexed, so we can filter or sort records easily.
Indexing is declared in the block YAML file using the `index_type` property.
Example:
```yaml
target: ajax
elements:
firstname:
type: BASIC
index_type: field
```
The `index_type` property can have two possible values:
`field`
: A field index matches exact values, and supports comparison queries, range queries, and sorting.
`text`
: A text index matches contained words (applicable for text values only).
Queries use the *CQE format* ([see documentation](https://github.com/repoze/repoze.catalog/blob/master/docs/usage.rst#query-objects).
Example (assuming `author`, `title` and `price` are existing indexes):
```python
context.app.search(
"author == 'Conrad' and 'Lord Jim' in title",
sort_index="price")
```
To reindex a record, we can use the Rapido Python API:
```python
myrecord.save() # this will also run the on_save event
myrecord.reindex() # this will just (re-)index the record
```
We can also reindex all the records using the `refresh` URL command:
```
http://myserver.com/Plone/@@rapido//refresh
```
#### Exercise 6: Compute The Top 5
We want to be able to sort the records according to their votes:
- we need to declare `total` as an indexed element,
- we need to refresh all our stored records,
- we need to update the `top5` block to display the first 5 ranked talks.
````{dropdown} Solution
:animate: fade-in-slide-down
:icon: question
We add the following to {file}`rate.yaml` containing:
```yaml
elements:
...
total:
type: BASIC
index_type: field
```
To index the previously stored values, we have to refresh the storage index by calling the following URL:
```
http://localhost:8080/Plone/@@rapido/rating/refresh
```
And to make sure future changes will be indexed,
we need to fix the {py:func}`like` function in the `rate` block:
the indexing is triggered when we call the record's {py:meth}`save` method:
```python
def like(context):
content_path = context.content.absolute_url_path()
record = context.app.get_record(content_path)
if not record:
record = context.app.create_record(id=content_path)
record['total'] = 0
record['total'] += 1
record.save(block_id='rate')
```
Now let's change the `top5` block:
- create {file}`top5.yaml`:
```yaml
elements:
talks:
type: BASIC
```
- create {file}`top5.py`:
```python
def talks(context):
search = context.app.search(
"total>0", sort_index="total", reverse=True)[:5]
results = []
for record in search:
content = context.api.content.get(path=record["id"])
results.append({
'url': content.absolute_url(),
'title': content.title,
'total': record["total"]
})
return results
```
- update {file}`top5.pt`:
```html
Talks Top 5
```
````
```{image} _static/rapido-top5.png
:align: center
```
### Create Custom Content-rules
Plone content rules allow triggering a given action depending on an *event*
(content modified, content created, etc.)
and on a *list of criteria* (for example: only for certain content types,
only in this folder, etc.).
Plone provides a set of useful ready-to-use content rule actions,
such as moving some content somewhere,
sending mail to an email address,
executing a workflow change, etc.
Rapido allows us to easily implement our own actions.
To do this, it adds a generic "Rapido action" to the Plone content rules system.
It allows us to enter the following parameters:
- The app id,
- The block id,
- The function name.
The {py:attr}`content` property in the function's {py:obj}`context` allows access to the content targeted by the content rule.
For instance, to transform the content title to uppercase every time we edit something,
we would use a function such as this:
```python
def upper(context):
context.content.title = context.content.title.upper()
```
#### Exercise 7: Reset The Votes On Workflow change
We would like to reset the votes when we change the workflow status of a talk.
We will need to:
- create a new block to handle our {py:func}`reset` function,
- add a content rule to our Plone site,
- assign the rule to the proper location.
````{dropdown} Solution
:animate: fade-in-slide-down
:icon: question
- create {file}`contentrule.py`:
```python
def reset(context):
talk_path = context.content.absolute_url_path()
record = context.app.get_record(talk_path)
if record:
record['total'] = 0
```
- go to {menuselection}`Site setup --> Content rules`, and add a rule for the event {guilabel}`Workflow state changed`,
- add a condition on the content type to only target *Talks*,
- add a Rapido action where the application is `rating`,
the block is `contentrule` and the method is `reset`,
- activate the rule for the whole site.
````
### Other Topics
The following Rapido features haven't been covered by this training:
- using Rapido blocks as tiles in Mosaic,
- using blocks as forms to create, display and edit records directly,
- access control,
- Rapido JSON REST API.
You can find information about those features and also interesting use cases in the [Rapido documentation](https://rapidoplone.readthedocs.io/en/latest/).