Clojure, Continued
Building a website with Clojure
Posted by 11/29/2009
So after writing a simple sql statement runner, I wanted to try writing a web application
in Clojure. I'm taking an existing site that I've written in Python using
Mako
and Selector
and SQLAlchemy, and trying to re-write it in Clojure.
I looked around for some things to help me with this task - these looked to be good candidates:
To start off I'm going to set up one simple page and one page with a list of objects stored in a postgresql database. It should have the beginnings of some html page inheritance of some sort. But I'm just getting started. And since I am just getting started let me say very emphatically that most of the code that follows will be terrible. I've found the only way to get past the "writer's block" of learning a new language it to simply write horrible code.
Why am I posting this then? Well, just to encourage people like me that might find themselves
intimidated by genius programmers writing perfect Clojure code.
Let me encourage all of you fellow mediocre programmers out there. Continue writing terrible code
for 6 months - and you'll know the language. Continue for
2 years and you'll be an expert. That's the way it works. Some people can write great code
in a few weeks - but the rest of us have to grind it out slowly.
So these are the 6 files I have started with.
config.clj # only database connection info for now
data.clj # sort of a DAO
site.clj # thing that starts the site up
templates.clj # the templates
urls.clj # the 'routes' or url mappings
views.clj # the 'views' (i.e. 'controllers' - I've just finished a big Django project and they
# call these functions 'views')
(ns site
(:use dk.bestinclass.clojureql))
(def *connection*
(make-connection-info "postgresql" "//localhost/walterklang" "--user--" "--password--"))
(load-driver "org.postgresql.Driver")
This is using clojureql to set up the connnection
(ns site
(:require config)
(:use dk.bestinclass.clojureql))
(def recordings
(run [*connection* rs]
(order-by (query recordings *) :ascending name)
(into-array rs)))
This is using clojureql to query a table. In this case the table
is a list of 'recordings'. I've put it into an array using into-array
for no particular good reason other than the recordset seemed to be a lazy set and
I couldn't figure out how to keep the connection open while still having this
exist as a standalone function
Why I wanted it a standalone function is not clear to me. I guess I wanted
to be able to see the list apart from an http response. NOTE: the
(:require config) line. That's one way to import the code
as if a module (using Python terminology) i.e. a library of functions.
(ns site
(:use [compojure.http.routes]
[compojure.server.jetty]
[compojure.http.servlet])
(:require urls))
(run-server {:port 9000}
"/*" (servlet my-app))
This just starts up the web application and runs it using jetty. This
is a fairly standard way to set up a compojure site.
What follows is by far the most code - and also by far the worst. I'm still
not sold on the idea of using a DSL to generate Html. I still prefer Mako
(which I could possibly still use by means of Jython). Or perhaps
StringTemplate. That being said,
I thought it would be a learning experience to try and see how
I would set up something resembling template inheritance and callable fragments - both
of which I had set up in the original Mako templates.
(ns site
(:use clj-html.core)
(:use clj-html.utils))
(defhtml title [] "Walter Klang") ;I'm setting up some default snippets here
(defhtml stylesheets []
[:link {:rel "stylesheet" :href "/stylesheets/screen.css"}]
[:link {:rel "stylesheet" :href "/stylesheets/style.css"}])
(defhtml javascripts []) ; more default snippets - see def in Mako
(defhtml feeds [])
(defhtml header []
[:h1#logo
[:a {:href "http://walterklang.com" :title "Walter Klang"}
[:img {:src "/images/walterklang-word-lightsource.gif"}]]])
;; just settings html attributes is the current folder matches a navigation item
(defn list-id [current match] (if (= current match) "active" match))
(defn link-id [current match] (if (= current match) "current" match))
(defhtml site-menu-item [name current match]
[:li {:id (list-id current match)} [:a {:id (link-id current match) :href (str "/" match)} name]])
;; set up a site menu of links
(defhtml site-menu [current]
[:ul#navlist
(site-menu-item "Home" current "")
(site-menu-item "Days" current "days")
(site-menu-item "Recordings" current "recordings")
(site-menu-item "Songs" current "songs")
(site-menu-item "Sessions" current "sessions")
(site-menu-item "Albums" current "albums")
(site-menu-item "History" current "history")])
(defhtml sidebar []
[:div#menu
[:h2 "Feeds"]
[:ul.stack_list "RSS"
[:li [:a {:href "/rss/mixes"} "Mixes"]]]])
(defhtml footer [])
(defhtml analytics [])
;; sort of a page inheritance mechanism using a macro. Not sure
;; if this is actually bad clojure though. it's very possible
(defmacro base-page [& body]
`(html
(str "<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>")
[:html
[:head
[:title (title)]
(stylesheets)
(javascripts)
(feeds)]
[:body
[:div.container
[:div#header (header)]
[:div#nav (navigation)]
[:div#content.content
(sidebar)
~@body]]
[:div#footer (footer)]
(analytics)]]))
(defn index-page []
#^{:doc "welcome page"}
(defhtml title [] "Walter Klang:Official Website")
(defhtml navigation [] (site-menu ""))
(base-page
[:h2 "Hello"]))
(defn recording-template [recording]
#^{:doc "snippet to render a single recording"}
(html
[:div.recording {:id (:id recording)}
[:p.name (:name recording)]]))
(defn recordings-page [context]
#^{:doc "list of recordings page"}
(defhtml title [] "Walter Klang:Recordings")
(defhtml navigation [] (site-menu "recordings"))
(base-page
[:h1 "Recordings"]
[:div#recordings
(map-str recording-template (:recordings context nil))]))
As you may notice, I've set up template inheritance by combining a macro base-page
and over-rideable functions per view function (i.e defhtml title).
The navigation section is ugly. I'm sending in a current folder as a parameter when I call the
function site-menu. I'm not very happy with how that looks, although I didn't really
like it in Mako either
(ns site
(:use [compojure.http.helpers])
(:require views)
(:require util))
(defroutes my-app
(GET "/*"
(or (serve-file (params :*)) :next))
(GET "/recordings" recordings-index)
(GET "/*" index)
(ANY "*"
(page-not-found)))
Next, the routes. Fairly typical compojure
(ns site
(:require templates)
(:require data))
(defn index
[request]
{:body (index-page)})
(defn recordings-index
[request]
(let [context {:recordings recordings}]
{:body (recordings-page context)}))
Finally, the controller code. I send in a context dictionary
to a template - and return that template as the :body response.
Conclusion
The only non-trivial code here is the templates.clj file, and I'm thinking
I don't like LISP syntax generating Html. I'm not sure why. It just doesn't feel right.
I'm going to look at blogjure and altlaw - to get an idea of how they do things, and thus how I should be doing things.
Comments
Post a comment