(iterate think (think))

Luminus progress report

08 Jan, 2013

The work on the framework continues steadily, and I've been integrating some of the feedback I got on the initial release.

I quickly discovered that simply using different files for template modules is insufficient. Many features need to update the project.clj with dependencies or other options.

To deal with this I made a util which reads in the project file and injects dependencies, plugins and other options. Now each plugin can add its own project elements independently of others.

I'm considering taking the same approach to managing the layout as well. For example, if bootstrap support was selected, then its js/css would be included in layout/common. Another use case would be to update the application routes if a module provided some new routes of its own.

I'd also like to highlight some of the additions to lib-noir. There are several new namespaces, such as noir.util.cache, noir.io, and noir.util.route. Let's take a look at each of these in turn.

Caching

Basic caching is provided via noir.util.cache. Cache allows wrapping any expr using (cache id expr), and the expr will only be evaluated if it's not found in the cache or if the cache has been invalidated. In case expr throws an exception the current cached value will be kept.

There are a couple of helpers for invalidating the cache. First, there's invalidate-cache!, which takes a key and removes it from the cache. Then, there's clear-cache! which removes all currently cached items.

It's also possible to set the timeout for cached items using set-cache-timeout! and passing it a value in seconds. If an item remains in the cache longer than the timeout, the cache will attempt to refresh the value by running the expr associated with the item.

Finally, you can set the maximum size of the cache by calling set-cache-size!, when the cache grows past the specified size, oldest items will be removed to make room for new ones.

I'm currently using the cache in Luminus for the documentation pages. Luminus fetches the documentation from github as markdown and then translates it to HTML. This is slow enough to be noticeable to the user. On top of that, github is known to have an occasional outage or two. :)

With this scheme, I can keep the docs up to date without having to redeploy the site, and I don't have to worry about the latency or github uptime.

IO

The noir.io namespace provides some helper functions to make it easier to handle static resources.

You can get the absolute path to the public directory of your application by calling resource-path.

If you need to read a file located in the public folder you can get a URL for the resource by calling get-resource and provided the path relative to the public directory.

If the resource is a text file, such as a markdown document, you can use slurp-resource to read it into a string.

Another addition is the upload-file function which saves the file generated by a multipart/form-data form POST to a path relative to the public folder. An example can be seen here:


(ns myapp.upload
  ...
  (:require [noir.io :as io]))
 
(defn upload-page []
  (common/layout
    [:h2 "Upload a file"]
    (form-to {:enctype "multipart/form-data"}
             [:post "/upload"]            
             (file-upload :file)            
             (submit-button "upload"))))
              
(defn handle-upload [file]
  (upload-file "/uploads" file)
  (redirect
    (str "/" (session/get :user) "/" (:filename file))))
   
(defroutes upload-routes
  (GET "/upload" [] (upload-page))
  (POST "/upload" [file] (handle-upload file)))

Access rules

Noir used to have a pre-route macro, which allowed for filtering and redirecting based on some rules.

Now, lib-noir provides a restricted macro which provides similar functionality.

You can define access rules as functions which accept the method, url, and params. The function then returns a boolean to indicate if the rule succeeded or not.

For example, if we wanted to restrict access to a page so that it's only accessible if the id in session matches the id in the page, we could write a rule like this:


(defn user-page [method url params] 
  (and (= url "/private/:id")
       (= (first params) (session/get :user))))

Then we wrap our handler in wrap-access-rules middleware. The middleware accepts one or more access rule functions, and checks if restricted pages match any of the rules provided.


(def app (-> all-routes
             (middleware/app-handler)
             (middleware/wrap-access-rules user-page)))  

With that in place, we can restrict access to our page as follows.


(restricted GET "/private/:id" [id] "private!")

Note that, you have to use noir.util.middleware/app-handler for wrap-access-rules to work correctly. Or manually bind the noir.request/*request*, eg:


(defn wrap-request-map [handler]
  (fn [req]
    (binding [noir.request/*request* req]
      (handler req))))

update I've since made wrap-request-map public in lib-noir, so if you need to wrap the request for any reason, you don't need to roll your own.

I hope you find the new features useful, and as always I'm open to feedback and suggestions for improvements as well as new features.



tags clojureluminus

comments


08 Jan, 2013 - anonymous

Thanks for making these changes, particularly the ones in luminus-template, I was thinking of contributing to the project, but then got put off when I realized that adding support for something meant overwriting project.clj of any previous required feature.

Oh, and thanks for adding support for Clojurescript! ;). Definitely needed that one.

You're really doing great stuff with this, I need to stop being lazy and start contributing.

09 Jan, 2013 - marc

I just wanted to say thanks for doing this, it has made my life much easier and I am looking forward to all the good stuff coming down the road.




help

*italics*italics
**bold**bold
~~foo~~strikethrough
[link](http://http://example.net/)link
super^scriptsuperscript
>quoted text
4 spaces indented code4 spaces indented code

preview

submit