Navigation

A template model provides data to a template through the syntax: m.modelname.property.

For example:

{# Get the site's title #}
{{ m.site.title }}
{# Fetch the title of the page with name page_home #}
{{ m.rsc.page_home.title }}
{# Fetch the title of the page whose id is the integer 1 #}
{{ m.rsc[1].title }}
{# Fetch the title of the page whose id is the template variable id #}
{{ m.rsc[id].title }}
{# Perform a search on all persons #}
{% for p in m.search[{query cat='person'}] %}{{ p.title }}{% endfor %}

Note

For the most frequently used model m_rsc a shortcut syntax is preferred. Instead of {{m.rsc[id].title}}, use {{id.title}}.

Models are Erlang modules that are prefixed with m_ and stored in your main module’s subdirectory models. For example, the model to access Zotonic resources (syntax m.rsc.property) is written in models/m_rsc.erl.

This function fetches a value from a model. Because there are quite some different variation how to use this function, it is good to understand a bit more about the inner workings of data lookup.

Let’s start with the parsing of a template expression:

{{m.rsc[1].is_cat.person}}

If you have done some work with Zotonic, you will be familiar with this syntax where we can find out if the resource with id 1 (the Administrator) is of category Person.

This expression contains 4 parts (separated by dots).

At the very start, the template parser resolves m.rsc to module m_rsc. The parser then calls m_rsc:m_find_value(Key,Source,Context) to fetch the value (where Key in our expression has the value of “1”).

It takes a Key and a data Source - a simple record containing 2 entities, model and value:

-record(m,{model,value}).

m_find_value often simply returns a value, but it may also return a (modified) data source record for the next call to m_find_value. m_rsc.erl resolves our expression in 3 different calls to m_find_value, where the data source record (“m” record) is used for pattern matching.

Step 1: The m record does not contain a value yet. The key (Id) is checked for visibility, and stored in the m record:

m_find_value(Id,#m{value=undefined}=M,Context)->...M#m{value=RId};

Step 2: With the m record now containing an Id value, the key is_cat is found. Again the key is stored in the m record:

Step 3: The next key to parse is person. Since this could have been any category name, a generic Key variable is used instead of an atom. The result is calculated in a function call, and returned for further manipulation (e.g. filters) or as string to the page:

All data calls to the OMDB go through the url http://www.omdbapi.com/?, with query string appended. We can pass the movie id, title, and pass a type (movie/series/episode). OMDB offers more parameters but we don’t need them now.

We will write our model in module models/m_omdb.erl. Let’s first get the mandatory elements out of the way:

-module(m_omdb).-behaviour(gen_model).-export([m_find_value/3,m_to_list/2,m_value/2]).-include_lib("zotonic.hrl").% ... We will add our m_find_value functions here% ... Before ending with the final fallback:m_find_value(_,_,_Context)->undefined.% This is the default m_to_list if we don't have any list values.% We will come back to this in a minutem_to_list(_,_Context)->[].% The function that we can ignorem_value(_,_Context)->undefined.

Before diving into the lookup functions, let’s see what we want to achieve as result.

Using m_find_value we will generate a list of query parameters, for example [{type,"series"},{title,"Dollhouse"}]

And pass this list to a “fetch data” function

That creates a URL from the parameters,

loads JSON data from the URL,

and transforms the JSON into a property list

The fetch_data function:

-specfetch_data(Query)->list()whenQuery::list().fetch_data([])->[{error,"Params missing"}];fetch_data(Query)->% Params title or id must be presentcaseproplists:is_defined(title,Query)orproplists:is_defined(id,Query)offalse->[{error,"Param id or title missing"}];true->% Translate query params id, title and type% into parameters that OMDB wantsQueryParts=lists:map(fun(Q)->make_query_string(Q)end,Query),Url=?API_URL++string:join(QueryParts,"&"),% Load JSON datacaseget_page_body(Url)of{error,Error}->[{error,Error}];Json->% Turn JSON into a property list{struct,JsonData}=mochijson2:decode(Json),lists:map(fun(D)->convert_data_prop(D)end,JsonData)endend.

It is important to know that we will pass a list, and get a list as result (for other template models this may be different).

The functions that will deliver our template interface are a bit more involved. From the template expressions we can discern 2 different patterns:

Expressions with 1 part:

m.omdb["Dollhouse"]

m.omdb[{querytitle="Dollhouse"}]

Expressions with 2 parts:

m.omdb.series["Dollhouse"]

m.omdb.series[{querytitle="Dollhouse"}]

When an expression is parsed from left to right, each parsed part needs to be passed on using our m record. For instance with m.omdb.series["Dollhouse"] we first tranform “series” to {type,"series"}, and then “Dollhouse” to {title,"Dollhouse"}, creating the full query [{type,"series"},{title,"Dollhouse"}].

fetch_data will return a property list, so we can write this to get all values:

{% for k,v in m.omdb["tt1135300"] %}{{k}}:{{v}}{% endfor %}

Handling the title is similar to the ID. Title must be a string, otherwise it would be a property key (atom):

% Syntax: m.omdb["some title"]m_find_value(Title,#m{value=undefined}=M,_Context)whenis_list(Title)->M#m{value=[{title,Title}]};% Syntax: m.omdb.sometype["some title"]% If no atom is passed it must be a title (string)m_find_value(Title,#m{value=Query}=M,_Context)whenis_list(Title)->M#m{value=[{title,Title}]++Query};

To parse the search expression, we can simply use the readymade property list:

... we can no longer pass around the m record; we must resolve it to a value and get the property value:

% Syntax: m.omdb[QueryString].title or m.omdb.sometype[QueryString].title% Key is in this case 'title'm_find_value(Key,#m{value=Query}=_M,_Context)whenis_atom(Key)->proplists:get_value(Key,fetch_data(Query));

We won’t do any validity checking on the parameter here, but for most modules it makes sense to limit the possibilities. See for instance how m_search:get_result is done.