ChicagoBoss example application: wiki

Update to use ChicagoBoss 0.8 on May 10, 2013.

Code is available on GitHub.

Download and build the latest ChicagoBoss source code:

git clone https://github.com/evanmiller/ChicagoBoss.git
cd ChicagoBoss
make

Make sure that ChicagoBoss successfully builds. Otherwise, you might need to download and install the latest version of Erlang and all it’s dependencies. To ask questions about ChicagoBoss, join the growing ChicagoBoss community on Google Groups.

Create an empty example wiki application by executing the following commands inside ChicagoBoss directory:

make app PROJECT=example_wiki
cd ../example_wiki/

Hello World
Let’s start with the “Hello World” smoke test and build from there. All shell commands are assumed to be executed from within the example_wiki directory.

Create example_wiki_pages_controller.erl file in src/controller directory

-module(example_wiki_pages_controller, [Req]).
-compile(export_all).

%% @doc show a "Hello World" message
index('GET', []) ->
	{output, "Hello World"}.

Start ChicagoBoss in development mode:

./init-dev.sh

and navigate to URL: http://localhost:8001/pages/index to see Hello World rendered in your browser.
The naming convention for ChicagoBoss controllers is: [OTP_application_name]_[controller_name]_controller.erl inside of src/controller directory.

Data Model
In this example, all the wiki data will be stored in MySQL database.
Modify db_host, db_port, db_adapter and add db_username, db_password, db_database in boss.config to result in:

[{boss, [
    {applications, [example_wiki]},
    {db_host, "localhost"},
    {db_port, 3306},
    {db_adapter, mock},
    {db_username, "root"},
    {db_password, "password"},
    {db_database, "example_wiki"},
    {log_dir, "log"},
    {server, mochiweb},
    {port, 8001},
    {session_adapter, mock},
    {session_key, "_boss_session"},
    {session_exp_time, 525600},
    {path, "../ChicagoBoss"},
    {vm_cookie, "my_secret_cookie"}, % Optional, defaults to abc123
    {websocket, true}
]},

{ tinymq, [
%% max_age- Maximum age of messages in the [message queue], in
%%   seconds. Defaults to 60.
    % {max_age, 60}
]},

{lager, [
    {handlers, [
      {lager_console_backend, info},
      {lager_file_backend, [
        {"log/error.log", error, 10485760, "$D0", 5},
        {"log/console.log", info, 10485760, "$D0", 5}
      ]}
    ]}
  ]},

{ example_wiki, [
    {base_url, "/"},
    {path, "../example_wiki"}
]}
].

Naturally, db_* tuple parameters will vary depending on your specific deployment of MySQL.
For more information on setting up database connectivity, see README_DATABASE.

Note: The "id" field should be a serial integer in README_DATABASE means that id column in that table is SERIAL which is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE

-- Table structure for table pages
CREATE TABLE IF NOT EXISTS `pages` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
  `page_title` varchar(32) NOT NULL,
  `page_text` varchar(1024) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;

To create a model for the pages table, create page.erl file in src/model directory.
Place the following into src/model/page.erl:

-module(page, [Id, PageTitle, PageText]).
-compile(export_all).

validation_tests() ->
	[{fun() -> length(PageTitle) > 0 end, "Page Title cannot be empty."},
	 {fun() -> length(PageTitle) =< 32 end, "Page Text cannot be more than 32 characters long."}].

Visit http://localhost:8001/doc/page to verify that the page model is available.
Let’s test adding content to MySQL database from the development console. After you start the server with ./start-dev.sh you find yourself in an erlang shell. Enter the following to create a new row in the pages table:

FirstPage = page:new(id, "First Page", "Page Content").
FirstPage:save().

“First Page” corresponds to the PageTitle parameter of the page.erl module, while “Page Content” corresponds to the PageText parameter of the page.erl module.
To load the saved page into a new variable, we use boss_db api such as:

[FoundPage] = boss_db:find(page, [{page_title, 'equals', "First Page"}], 1).

The URLs in the wiki are going to be:

  • /pages/index displays the list of all wiki pages
  • /pages/view/page-id displays a wiki page
  • /pages/create allows to add a page
  • /pages/edit/page-id allows to edit a page

This is a basic application example, and all the business logic will reside inside of one controller.

Let’s go back and modify the pages controller, index function, to return a list of pages in our wiki.

Edit src/controller/example_wiki_pages_controller.erl:

-module(example_wiki_pages_controller, [Req]).
-compile(export_all).

%% @doc show a list of all wiki pages out there
index('GET', []) ->
	Pages = boss_db:find(page, []),
	{ok, [{pages, Pages}]}.

[Req] parameter in the module declration above creates a Req variable which is a simple_bridge request object with all the functions that it’s API has at https://github.com/nitrogen/simple_bridge.

When a controller’s function returns a tuple in the {ok, []} format, ChicagoBoss pushes this data into a view which is developed using ErlyDTL.

Views are created as src/view/[controller_name]/[function_name].erl files.

Inside the src/views directory create pages directory and add the following index.html file:

<html>
<head>
<title>{% block title %}Wiki pages{% endblock %}</title>
</head>
<body>
{% block body %}
<h1>My Wiki Pages:</h1>
<ul>
{% if pages %}
  {% for page in pages %}
    <li><a href="view/{{page.id}}">{{ page.page_title }}</a></li>
  {% endfor %}
{% else %}
  <li>No Wiki Pages</li>
{% endif %}
</ul>
{% endblock %}
</form>
</body>
</html>

After you save index.html and browse to http://localhost:8001/pages, you will get an HTML page with a list of page titles or “No Wiki Pages” depending on if you manually added pages to the MySQL pages table.

When you browse to http://localhost:8001/pages/index or http://localhost:8001/pages, a GET request gets sent to the server by the browser and ChicagoBoss routes the request to index function inside the example_wiki_pages_controller.

Line Pages = boss_db:find(page, []) selects all rows from pages table, assigns it to Pages variable and the controller’s function returns the results packed inside a tuple to the index.html view.

The returned data of controller’s functions can be different tuples:

  • {output, <<"Raw Output">>} will bypass views and send back raw data as is to the browser
  • {json, [{pages, Pages}]} will bypass views and send back json encoded data. This functionality makes it extremelly easy to build REST-based APIs
  • {ok, [{pages, Pages}, {stuff, EvenMoreContent}]} will send the tuples inside of the list to the view
  • More example of return values

Just for kicks, change the return result of example_wiki_pages_controller:index/2 to be {json, [{pages, Pages}]} instead of {ok, [{pages, Pages}]} and load the URL in your browser: http://localhost:8001/pages to see JSON representation of all pages inside of the MySQL database.

We need to be able to create, view and edit wiki pages using the browser. Let’s edit src/controller/example_wiki_pages_controller.erl to allow this functionality.

When we want to see a wiki page, the page’s id will be appended to the /pages/view/ URL. View function will look-up the page in the database, if the page is found, page_text() function will be invoked on the object to get the page’s content. When new pages are created, it is possible to link from one page to another by adding [page-id] markup to it’s content, and the hackish/ugly/smelly code inside of the view function replaces [page-id] format into hrefs to the page and sends the data to the view.html view.

%% @doc display a specific wiki page
view('GET', [Id]) ->
	case boss_db:find(Id) of
		{error, Reason} -> {redirect, [{action, "create"}]}; %% TODO: Redirect to error page
		undefined -> {redirect, [{action, "create"}]}; % When you visit /view/NotExistentPage the requested Page doesn't exist, we redirect the client to the edit Page so the content may be created
		ExistingWikiPage -> 
			% Replace all [page-id] with links
			% TODO: There has to be a better way
			StartHrefs = re:replace(ExistingWikiPage:page_text(), "\[\w*-*[0-9]*", "<a href='/pages/view/&'>&", [global, {return, list}]),
			ClosedHrefs = re:replace(StartHrefs, "\]", "</a>", [global, {return, list}]),
			CleanedUp = re:replace(ClosedHrefs, <<"\[">>, "", [global, {return, list}]),
			{ok, [{page, ExistingWikiPage}, {cleaned, CleanedUp}]}
	end.

Accordingly, src/view/pages/view.html needs to be created to display the wiki page content:

{% extends "pages/index.html" %}
{% block title %}{{ page.page_title }}{% endblock %}
{% block body %}
<h1>{{ page.page_title }}</h1>
<div>{{ cleaned }}</div>
<ul>
<li><a href="{% url action="edit" %}/{{ page.id }}">Edit</a></li>
</ul>
{% endblock %}

When creating links in view erl files, keep in mind that links are relative to two different directories.

Note about relative links in view files

View files HTML references such as img, script are relative to the /priv directory:

<img src="/static/pics/something.png" />
<!-- The server will send something.png from <project>/priv/static/pics/something.png -->

ErlyDTL markup references, those inside {% %} tags, such as {% extends %} are relative to the /src/view directory:

{% extends "pages/index.html" %}
<!-- ErlyDTL will try to load the ErlyDTL artifact in <project>/src/view/pages/index.html  -->

When the wiki visitor wants to create a new page, URL /pages/create needs to be visited. The controller has the following function

%% @doc Handles rendering the new wiki page view which is empty by default
create('GET', []) -> ok;

which is a place holder that does nothing right now, and just makes ChicagoBoss send back the src/view/pages/create.html view data:

{% extends "pages/index.html" %}
{% block title %}A new Wiki Page{% endblock %}
{% block body %}
{% if errors %}
<ul>
  {% for error in errors %}
  <li>{{ error }}</li>
  {% endfor %}
</ul>
{% endif %}
<form method="post">
<h1>Create a new wiki page</h1>
<div>
<p>Title:</p><textarea name="page_title">{% if new_page %}{{ new_page.page_title | escape }}{% endif %}</textarea>
</div>
<div><p>Text:</p>
<textarea name="page_text">{% if new_page %}{{ new_page.page_text | escape }}{% endif %}</textarea>
</div>
<input type="submit"/>
</form>
{% endblock %}

Create.html view template contains a form that POSTs data to /pages/create URL and that means that our controller needs to be able to receive the POST content, have the model validate the data and insert the data of a new wiki page as a new row into the pages table. If the data validates and is inserted, we redirect the browser to the controller’s view() function with the new page id as a parameter and if there is an error, the create.html pages gets rendered again but this time with an error message and title and pages textarea populated to previous values.

%% @doc Handles POST data of form submission from new wiki page
create('POST', []) ->
	Title = Req:post_param("page_title"),
	Text = Req:post_param("page_text"),
	NewWikiPage = page:new(id, Title, Text),
	case NewWikiPage:save() of
		{ok, SavedWikiPage} -> 	{redirect, [{action, "view"}, {id, SavedWikiPage:id()}]}; 
		{error, ErrorList} -> {ok, [{errors, ErrorList}, {new_page, NewWikiPage}]}
	end.

The view.html template contains a link to be able to edit a wiki page. URL to edit a wiki page is in the /pages/edit/page-id format. edit(‘GET’, [Id]) function in the controller gets invoked. It looks up the page in the database and passes it’s data to the src/view/pages/edit.html template

	
%% @doc Fetch the existing wiki and show the edit page
edit('GET', [Id]) ->
	ExistingWikiPage = boss_db:find(Id),
	{ok, [{page, ExistingWikiPage}]};  

The src/view/pages/edit.html contains a form that allows the visitor to update a wiki page.

{% extends "pages/index.html" %}
{% block title %}Edit wiki page{% endblock %}</title>
{% block body %}
<h1>Edit your wiki page:</h1>
<p>To link between pages, place the destination page numerical id inside of square brackets.</p>
<p><i>Example:</i> <b>Please visit [page-number] to view more information.</b></p>
<form method="post" action="{% url action="edit" %}">
<input type="hidden" name="page_id" value="{% if page %}{{ page.id }}{% endif %}" />
<p>Title:</p><input type="text" name="page_title" value="{% if page %}{{ page.page_title }}{% endif %}" />
<p>Text:</p><textarea name="page_text">{% if page %}{{ page.page_text }}{% endif %}</textarea>
<div>
<input type="submit" value="Save"/>
</div>
</form>
{% if errors %}
<ul>
  {% for error in errors %}
  <li>{{ error }}</li>
  {% endfor %}
</ul>
{% endif %}
{% endblock %}

The example_wiki_pages_controller.erl controller handles POST data submission from the /pages/edit/page-id URL.

%% @doc Updates the wiki page from the Edit view's POST information	
edit('POST', []) ->
	Id = Req:post_param("page_id"),
	Title = Req:post_param("page_title"),
	Text = Req:post_param("page_text"),
	ExistingWikiPage = boss_db:find(Id),
	UpdatedWikiPage = ExistingWikiPage:set( [{page_text, Text}, {page_title, Title}] ),	
	case UpdatedWikiPage:save() of
		{ok, SavedWiki} -> 	{redirect, [{action, "view"}, {id, Id}]}; % Redirect to the updated page
		{error, ErrorList} -> {ok, [{errors, ErrorList}, {page, UpdatedWikiPage}]}
	end.

Example_wiki can be extended with the following functionality:

  • Admins only area to delete wiki pages to demonstrate sessions and cookies
  • Upload images functionality to demonstrate multipart/form-data functionality
  • Live stream of latest changes to demonstrate publish/subscribe functionality of BossNews and BossMQ
  • Unit testing to demonstrate boss_web_test

14 thoughts on “ChicagoBoss example application: wiki”

  1. Thank you for the feedback. I updated the tutorial to account for the missing index.html.

    Regarding the edit/2 function, I need to rework the tutorial with a better explanation how page editing process works: GET to /edit/page-id brings back a page with form controls that allow modifying the wiki page content. When we POST from that page, the POST goes to /edit URL also and if the data passes validation and gets persisted to the database, the controller redirects to /view/page-id URL otherwise the /edit/page-id shows back up with error messages

  2. [img src="static/pics/something.png" /]

    – need to leading slash,
    such as:

    [img src="/static/pics/something.png" /]

    Without slash CB says that a: “The requested template (“src/view/[view_name]/static.html”) was not found.”

  3. Dear sir,

    I’m already used to use eclipse as my IDE (I’m a java programmer most of the time), and I have been using Erlide for learning erlang OTP in the past 8 months (no significant projects up until now he he he).
    I found your framework is very much like java MVC, and its great … really great!
    Can you please show us (Erlide users) how to create an erlang web application, using your marvelous framework?

    Thanks
    Bromo

  4. When I execute make in example_wiki, I got this error:
    make: *** No targets specified and no makefile found, Stop.
    Please help

    1. I see what the problem was. You are using a later version of ChicagoBoss than what the original article was written for. I just updated the article to use ChicagoBoss v 0.8.

  5. Point being that the example can not be followed because make app PROJECT=example_wiki does not generate ../example_wiki/Makefile

    With the amount of errors, typos, and missing functionality in this example, it seems that it was just typed right off the top of the head into the wiki and not actually tested at all. It should either be corrected or taken offline so that it doesn’t turn up in search results.

    1. I updated the instructions and code to use ChicagoBoss v0.8. The original article was using CB 0.6 or even earlier. Thank you for pointing out the problem.

  6. Hi,

    First of all, awesome tutorial. So thanks!

    Secondly, I have a question about the web sockets example (the “service” action in the pages controller). Things doesn’t seem to be working. I see the the client test page is able to establish a socket connection and messages are indeed passed from the client, however, this does not reflect in any way on the server (it’s like it’s not processing the data sent from the client). Any ideas?

    Thanks

  7. Shouldn’t line 5 of the boss.cfg file say “mysql” instead of “mock”?

    Also, you afterwards say this: “Visit http://localhost:8001/doc/page to verify that the page model is available.”, how can we verify the page model is available? At that point of the tutorial I get something like the equivalent of a Javadoc output, and lots of CRASH REPORT at the terminal.

    18:05:16.823 [error] CRASH REPORT Process with 0 neighbours crashed with reason: no match of right hand value {error,enoent} in boss_web_controller_handle_request:make_etag/3 line 81

  8. Good tutorial. Some issues that I met.
    1. When trying json, I changed ‘ok’ to ‘json’, it’s not working. Page is “Aborted”(http status). Don’t know why.
    2. In the “view” action of the “re:replace” part, I had to use double back slashes to make it work. Your code on github is correct.
    3. The replacing seems working. But I the charactors has been converted into html spcecials displayed on the webpage instead of link: visit <a href='/pages/view/page-3'>page-3</a>

Comments are closed.