MVC in GNU Artanis
MVC is a classical design pattern that separates the application into three main components: Model, View, and Controller. The Model is responsible for managing the data, the View is responsible for displaying the data, and the Controller is responsible for handling the user input and updating the Model and View accordingly.
In this tutorial, we will show how to use MVC in GNU Artanis. We will create a simple blog system that allows users to create, read, update, and delete blog posts. We will use the Model to manage the blog posts, the View to display the blog posts, and the Controller to handle the user input.
The code of project is here: https://gitlab.com/NalaGinrut/mvc-blog-example
The OS is assumed to be Ubuntu 24.04 LTS.
Figure 1: Architecture of MVC
Create Artanis app and configure the database
art create
Modify conf/artanis.conf to configure the database connection.
db.enable = true db.type = sqlite3
To simlify the example, we use SQLite3 as the database. Make sure you have the SQLite3 installed.
sudo apt-get install sqlite3
Create Model
In this simple blog system, we only need one model to manage the blog posts. Let's create the model.
art draw model article user # drawing model article # working Models `article.scm' art draw model user # drawing model user # working Models `user.scm'
We create two models:
- article in app/models/article.scm
- user in app/models/user.scm
create-artanis-model model_name
Now let's modify article.scm to add the following defniton:
(create-artanis-model article (:deps user) (id integer (#:primary-key #:auto-increment)) (title char-field (#:not-null #:maxlen 128)) (timestamp bigint (#:unsigned #:not-null)) (content longtext))
(create-artanis-model model_name (:deps models ...) table-definition)
- When booting the application with
art work
, Artanis will automatically create the tables in the database based on the model definitions. - Here we have to specify
(:deps user)
to make sure the user model is created and loaded before the article model.
Then let's modify user.scm to add the following defniton:
(create-artanis-model user (:deps) (id integer (#:primary-key #:auto-increment)) (username char-field (#:not-null #:maxlen 128)) (password char-field (#:not-null #:maxlen 512)) (salt char-field (#:not-null #:maxlen 8)) (email char-field (#:not-null #:maxlen 128)))
- salt is critical here, it is used to generate the password hash.
- GNU Artanis will automatically detect the salt field and generate the password hash when you call its authenticating API. Of course, it's customizable.
Add default user
Usually, before we run the application, it's often necessary to add some default contents, for example, the default user, and welcome article. This is what migration is for.
art draw migration user # drawing migration user # working Migration `user_20241207151014.scm' art draw migration article # drawing migration article # working Migration `article_20241207151048.scm'
Note: The migration file name always contains a timestamp for version control.
Then let's modify db/migration/user20241207151014.scm to add the following defniton:
(define (add-test-user! name password salt) (let ((mt (map-table-from-DB (get-conn-from-pool!))) (real-pass (string->sha-256 (string-append password salt)))) (mt 'set 'user #:username name #:email (format #f "[email protected]" name) #:password real-pass #:salt salt)) (migrate-up (display "Adding user `admin'...") (add-test-user! "Admin" "1234567" "asdf") (display "done.\n"))
Now let's modify db/migration/article20241207151048.scm to add the following defniton:
(define (add-welcome-article!) (let ((mt (map-table-from-DB (get-conn-from-pool!)))) (mt 'set 'article #:title "Welcome to Artanis" #:timestamp (current-time) #:content "This is a simple blog system powered by GNU Artanis."))) (migrate-up (display "Adding welcome article...") (add-welcome-article!) (display "done.\n"))
migrate-up
is the function that will be called when the commandart migrate up
is executed.
Now we run the migration to add the default user and welcome article.
art migrate up user # Loading /home/nalaginrut/Project/mvc-example-blog/conf/artanis.conf...done. # connection pools are initilizing...DB pool init ok! # Now the size of connection pool is 64. # [Migrating user_20241207151014.scm] # Creating table `user' defined in model # ...... # Done. # Adding user `admin'...done. art migrate up article # Loading /home/nalaginrut/Project/mvc-example-blog/conf/artanis.conf...done. # connection pools are initilizing...DB pool init ok! # Now the size of connection pool is 64. # [Migrating article_20241207151048.scm] # Creating table `user' defined in model # ...... # Done. # Creating table `article' defined in model # ...... # Done. # Adding welcome article...done.
Trouble shooting
You may encounter the following error when running the migration:
Throw to key `artanis-err' with args `(500 #<procedure DB-query (conn sql #:key check?)> "failed reason: `~a'~%" "database is locked")'.
This is because SQLite3 does not support multiple write operations at the same time. To solve this problem, you can enable Write-Ahead Logging (WAL) mode for the database.
sudo apt install sqlite-utils sqlite-utils enable-wal blog.db
Create Controller and Views
Index
First, we need index in default:
art draw controller index # drawing controller index # working Controllers `index.scm' # create app/controllers/index.scm # working Views `index'
Let's modify app/controllers/index.scm to add the following defniton:
(import (app models article) (srfi srfi-1)) ; for fold (define blog-title "Artanis blog-engine") (define footer (tpl->html `(div (@ (id "footer")) (p "Artanis blog-engine based on " (a (@ (href "https://github.com/HardenedLinux/artanis")) "GNU Artanis") ".")))) (define (show-all-articles articles) (fold (lambda (x prev) (cons `(div (@ (class "post")) (h2 ,(result-ref x "title")) (p (@ (class "post-date")) ,(strftime "%Y-%m-%d" (localtime (result-ref x "timestamp" #:decode? #f)))) (p ,(result-ref x "content"))) prev)) '() articles)) (define (article-get-all) (cond (($article 'get #:ret 'all #:order-by '(timestamp desc)) => show-all-articles) (else '()))) (index-define "" (lambda (rc) (let ((all-posts (tpl->html (article-get-all)))) (view-render "index" (the-environment))))) (get "/" (lambda (rc) (redirect-to rc "/index")))
- The
(index-define your_url handler
function is used to define the action for the index controller. The final URL will be /index/{yoururl}. Here the URL is empty""
, so the final URL will be /index. (view-render path (the-environment))
is used to render the view template. The first argument is the view template name, and the second argument is always(the-enviroment)
, which is a special macro that expands environment information used by GNU Guile runtime.- The view path is automatically located in app/views/index/index.html.tpl.
- To be convenient, we add a redirect to the index controller for root URL.
After importing the article model, there's a special function $article
which is the Relational Mapping of the table article. This function is automatically generated by GNU Artanis based on the model definition, the naming convention is $modelname.
You may be familiar with the Object-Relational Mapping (ORM) in other frameworks, which is mapping Object Oriented Programming (OOP) to Relational Database Tables. However, ORM is just one of the types of Relational Mapping. GNU Artanis uses Functional Programming paradigm for Relational Mapping to map the database tables to Scheme closures, which is more flexible and lightweight.
Now let's create the view template app/views/index/index.html.tpl:
<html> <@include header.html.tpl %> <body> <div id="container"> <div id="main"> <%= all-posts %> </div> <div id="sidebar"> <form method="get" id="dashboard" action="/dashboard/admin"> <div><button>dashboard</button></div> </form> <div id="navigation"> <h3>Navigation</h3> <ul> </ul> </div> </div> </div> <%= footer %> </body> </html>
Dashboard
Hmm…now we need a dashboard to submit a new article. I confess it's over simplified, and lacks the post editing and deleting functions. I'll leave it to you as an exercise.
art draw controller dashboard # drawing controller dashboard # working Controllers `dashboard.scm' # create app/controllers/dashboard.scm # working Views `dashboard
We modify app/controllers/dashboard.scm to add the following defniton:
(import (app models article)) (define (gen-login-page rc) (let ((failed (params rc "failed"))) (view-render "login" (the-environment)))) (dashboard-define "admin" (options #:with-auth gen-login-page) (lambda (rc) (view-render "admin" (the-environment)))) (dashboard-define "login" (options #:session #t) gen-login-page) (dashboard-define "new_post" (method post) (options #:with-auth gen-login-page #:from-post #t) (lambda (rc) ($article 'set #:title (:from-post rc 'get "title") #:content (:from-post rc 'get "content") #:timestamp (current-time) #:user_id 0) (redirect-to rc "/"))) (dashboard-define "auth" (method post) (options #:auth '(table user "username" "password") #:session #t) (lambda (rc) (cond ((or (:session rc 'check) (:auth rc)) (let ((referer (get-referer rc #:except "/dashboard/login*"))) ; (if referer (redirect-to rc referer) (redirect-to rc "/dashboard/admin")))) (else (redirect-to rc "/dashboard/login?login_failed=true")))))
#:with-auth gen-login-page
is used to specify the login page. If the user is not logged in, it will redirect to the login page.#:session #t
is used to enable the session. GNU Artanis will automatically create a session for the user.#:from-post #t
is used to get the post data from the request.
Now we add the view templates app/views/dashboard/admin.tpl:
<html> <@include header.html.tpl %> <body> <p>edit your article</p> <form id="post_article" action="/dashboard/new_post" method="POST"> <p>title:</p> <input type="text" name="title"/></br> <p>content:</p> <textarea name="content" rows="25" cols="38">write something</textarea></br> <input type="submit" value="Submit"/> </form> </body> </html>
Login view
The login view is located in app/views/dashboard/login.tpl. There's a little bit different, we need to handle the failed login case.
<html> <@include header.html.tpl %> <body> <%= (if failed (tpl->html `(p (font (@ (color "red")) "Invalid username or passwd!"))) "") %> <div id="container"> <div id="main"> <form id="login" action="/dashboard/auth" method="POST"> <p>username: <input type="text" name="username"></p> <p>password : <input type="password" name="password"></p> <input type="submit" value="Submit"> </form> </div> </div> </body> </html>
Header
Finally, we need a general pub/header.html.tpl in default:
<head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title> GNU Artanis blog engine </title> <@css common.css %> </head>
Public directory
In GNU Artanis, the pub directory is used to store static files such as images, CSS, and JavaScript files. The files in the pub directory are accessible directly from the browser.
Login to the dashboard
After creating the dashboard controller, we need to login to the dashboard to submit a new article. The default username is Admin and the password is 1234567.
Run the application
OK, now we have finished the blog system. Let's run the application.
art work
Now open your browser and visit http://localhost:3000.