Go to page content

Andreas Stenius ASTEKK AB - blog site

Article — , 01 June 2011

Writing your own Zotonic module -- part IV

It's time to improve our filter from part III, which will also bring in a template, a dispatch rule and a model.

We're building on the source from part III.

The direction for the list_links filter

The filter from part III was really quite simple. I would like to be able to extract the various parts of the link in the template for loop. One way would be to have the filter return a tuple with all data parsed out, but that is a bit too rigid, and doesn't welcome future changes.

Instead, I'm planning to use a model to hold the link, and only parse out the required data upon request.

If this didn't make much sense yet, keep reading, and if by the end it still doesn't make sense, let me know, and I'll have to clarify myself.

 

Updated filter

Ok, so the changes needed to get model values from the filter are rather minor. It needs to return a m record pointing to our yet to be developed model, and the link string.

So, here it is:

%% file: mod_gazonk/filters/filter_list_links.erl
-include_lib("zotonic.hrl").
list_links(In, _Context) ->
  case re:run(In, "<[aA][^>]*>[^<]*</[aA]>", \
	[global, {capture, first, binary}]) of
    nomatch -> [];
    {match, M} -> [#m{model=m_list_links, value=V} || [V] <- M]
  end.

All we have to do now is to implement the model, m_list_links, and then add some template and dispatch rules to show it off.

 

Dispatch

The easiest part first, to route a request to our yet to be written template for testing the list_links filter and model.

Dispatch rules are specified as a list of tuples (using erlang syntax, but don't give the file a .erl extension, or zotonic will attempt to compile the file as a regular erlang module, which it is not).

The zotonic documentation covers URL DISPATCH RULES pretty well already, so I'll simply give the simple rule we need for the demo here:

%% Here is the code for the file mod_gazonk/dispatch/dispatch 
%% from the source repo:
%% -*- mode: erlang -*-
%% mod_gazonk dispatch rules

[
  {gazonk, ["gazonk"], resource_template, [ {template, "gazonk.tpl"} ]}
]. 

This rule is called gazonk, and it matches the request path of /gazonk which is passed on to resource_template with a single argument of {template, "gazonk.tpl"}, telling it to use the gazonk.tpl template file, which I'll show you next.

 

Templates

What would we do without them? Jokes apart, templates are the glue between the server side code and the client side GUI/code.

I'm surprised to see that the zotonic documentation has no introduction on templates themselves, but merely lists the various tags, scmops (screen components), filters, validators and actions you can use in your templates.

The template engine is based on ErlyDTL, as is described on zotonic.com. ErlyDTL in turn is an implementation of the Django Template Language. So both of these sites has good documentation on the template syntax, while the zotonic documentation has good reference documentation for the zotonic specifics and quirks.

Now, to kick off this show, lets add some boiler plate code to mod_gazonk/templates/gazonk.tpl:

{% extends "base.tpl" %}
{% block content %}
<h1>This is mod_gazonk/templates/gazonk.tpl</h1>
 
{% endblock %} 

We build upon the base.tpl file so we don't have to care about the entire html page, which also means we're in line with whatever style the site has overall.

With your module active, browse to http://yoursite/gazonk. If all is working, it should present you with a heading stating that This is mod_gazonk/templates/gazonk.tpl, as we put into the template.

When that is working, we can see that the list_links filter is doing its job properly by adding this before the endblock tag in the template:

<h2>Print m.rsc.page_home.body|list_links</h2>
 {% print m.rsc.page_home.body|list_links %}

Now, this will be an empty list if your page_home doesn't have any links in the body text. If that is the case, you can either use another page/resource/property which has links, or use some static text directly in the template:

{% print "some text <a href='http://blog.astekk.se'>ASTEKK Blog</a> \
 which should not be part of the value"|list_links %}

Now, we need the list_links model before we can use the value returned by the list_links filter.

 

Models

Models have a very simple interface, but don't get fooled by it's simplicity. It's ingenious design allows a very wide and flexible use (which could be abused, as I'll show in a moment).

Zotonic defines a gen_model behaviour, exporting three functions: m_find_value/3, m_to_list/2 and m_value/2.

m_find_value/3 is the real worker when it comes to models. It is responsible for looking up key (property) values for what ever it is a model for.

m_to_list/2 is used by the for loop to turn the model value into a list, ready for iteration. Not all models (or values) make sense to iterate over, in which case this function could return the empty list.

m_value/2 should give the plain version of the value, I guess. But this function isn't called so I'm not really sure.

First, we need to create the model file, with some boiler code to make it a valid erlang module:

%% file: mod_gazonk/models/m_list_links.erl
%%
%% This code is public domain.
%% Andreas Stenius, 2011.

-module(m_list_links).
-author("Andreas Stenius <andreas.stenius@astekk.se>").
-behaviour(gen_model).
-include_lib("zotonic.hrl").
-export([m_find_value/3, m_to_list/2, m_value/2]).

I'll add m_value/2 and m_to_list/2 first, since they are the simplest (and my m_find_value/3 relies on them):

m_to_list(#m{value=V}, _Context) ->
    case re:run(V, "(\\w+)=(['\"]?)(.*?)\\2(\\s|>)", \
		[global, {capture, [1,3], list}]) of
	nomatch -> [];
 	{match, M} -> [ {Prop, Val} || [Prop, Val] <- M ]
    end.

m_value(#m{value=V}, _Context) -> V.

m_value/2 simply returns the string value (the raw link) extracted by the filter from the source text; while m_to_list/2 collects all anchor tag properties with a regular expression, putting it in a property list. Note there is a weakness here in that it may look beyond the closing angle bracket of the <a> tag. I've chosen not to bother with it for now.

Next up is the m_find_value/3:

m_find_value(value, #m{}=M, Context) ->
    m_value(M, Context);
m_find_value(text, #m{value=Link}, _Context) ->
    {match, [M]} = re:run(Link, ">(.*)</.>", [{capture, all_but_first, binary}]),
    M;
m_find_value(Prop, #m{} = M, Context) ->
    proplists:get_value(atom_to_list(Prop), m_to_list(M, Context)).

I've chosen to give value and text properties special meaning for list_links model values.

Value will return the entire anchor text that was extracted from the source text (this is the same as the filter returned before we changed to return model values) using the m_value/2 (could just have used the same code as m_value/2 does, but I didn't want to repeat the functionality).

Text will return the link text between the opening and closing anchor tags.

The third fun clause of m_find_value/3 is for extracting any tag property of the anchor tag, using the property list returned by m_to_list/2.

That's it. We can now try this out in our sample gazonk.tpl template!

 

Testing m.list_links

I've written up some for loops to show the various parts of the link model value. Here are some snippets. See the source code for all the gory details.

{% for link in text|list_links %}
	<hr />Debug link value: {% print link %}
	Text: {{ link.text }}<br />
	Value: {{ link.value }}<br />
	Url: {{ link.href }}<br />

	<h3>Props loop</h3>
	{% for Prop, Value in link %}
		{{ Prop }} = {{ Value }}<br />
	{% endfor %}
{% endfor %}

And this could give output similar to:

Debug link value:
 {m,m_list_links,
   <<"<a alt='my alt caption' href='http://blog.astekk.se'>ASTEKK Blog</a>">>}

 Text: ASTEKK Blog 
Value: ASTEKK Blog 
Url: http://blog.astekk.se 

Props loop

alt = my alt caption href = http://blog.astekk.se

 

Abusing(?) m_find_value/3

In the source you'll see I have three more function clauses. These are to bypass the list_links filter, or rather, apply it implicitly on the data passed to the model.

Here is the code any way:

m_find_value(S, #m{value=undefined}, Context) when is_list(S) \
					orelse is_binary(S) ->
	filter_list_links:list_links(S, Context);
m_find_value(Id, #m{value=undefined} = M, Context) ->
	m_rsc:m_find_value(Id, M, Context);
m_find_value(Key, #m{value=Id} = M, Context) when is_integer(Id) ->
	case m_rsc:m_find_value(Key, M, Context) of
		undefined -> undefined;
		S -> filter_list_links:list_links(\
				?__(S, Context), Context)
	end;

The first clause takes a string (list or binary) and passes it through the filter. This is the same as applying the filter to it directly, so it doesn't serve any real purpose besides showing it is possible.

The second clause looks up a resource using the rsc model (this is cool!), and the third is to finish it off by looking up a property on that resource. A little more code would be needed to support nested lookups as the rsc model does (i.e. m.rsc[id].author.body).

The ?__() is a macro defined in zotonic.hrl to support translations.

{# You can now also write: #}
m.list_links.page_home.body
{# which will have the same effect as #}
m.rsc.page_home.body|list_links
{# or #}
m.list_links["some links: <a>foo bar</a>"]
m.list_links[id].body
   

 

 

Concluding remarks

This has been the most interesting part yet (for me any way ;c). The code is in no way optimal or written according to any best practices. It merely shows how to tie your shoes, so you can walk out of here.

See the hg repo for the full source code.

Stay tuned for further articles on this and related topics (but don't hold your breath).

Also, I've begun using twitter, which will be an easy way to get notified of any updates.

Thank you for reading,

Andreas Stenius

Comments

  • avatar

    JD

    Posted 1 year, 1 month ago.

    Thanks for this tutorial - exactly what I was looking for and very helpful :)

  • avatar

    Andreas Stenius

    Posted 9 months, 27 days ago.

    You're welcome :). I really should pick this up and extend with new topics.