Building a Social Media site

2011-01-09 10:30:26

Following my now dated "Reddit Clone" tutorial, I've made a revised version which demonstrates how easy it is to build interactive websites using  Moustache, Enlive and ClojureQL. 

 

Preface

After many requests I've decided it would be a good idea to build a Clojure Driven Social Media Community website using some of the hottest tools available today to help us pull together. If you've read my old "Reddit Clone in 10 minutes and 91 lines of Clojure" this is on a very similar thread, however the core is now 98 lines and the tools have been updated. Lets get to it.

 

Frontpage

We begin by simply getting a webserver (Jetty) up and running and set to render our frontpage. One of the great things about developing webapps using Ring/Moustache is that you can construct your source such that when loaded on your development machine, it'll spawn a Jetty instance, but when loaded as a war file it will run as a Servlet within Tomcat. This makes for insanely rapid development/deployment. First, we'll start building our namespace which contains all of our web templates - I usually start by building a master page, which all (most) other pages are derived from:

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Clojure Driven Social Site</title>
    <script type="text/javascript" src="/scripts/jquery.js"></script>
    <script class="import" type="text/javascript"></script>
    <link rel="stylesheet" href="/css/main.css" type="text/css" media="screen" />
    <link class="style"  rel="stylesheet" type="text/css" media="screen" />
  </head>

  <body>
    <div class="header">
      <img src="/images/logo.jpg"/>
    </div>

    <div id="content">
    </div>

    <div class="footer">
      <a href="/">home</a>
      <a href="/submit">submit a link</a>
      <a id="loginout"/>
    </div>

  </body>
</html>

This HTML serves as a skeleton where Enlive will inject the dynamic components. Notice the places holders for scripts, styles and content which Enlive will look for. What we want now, is a function which takes 3 arguments: The scripts and styles to be loaded in head and the content for the div tag with that id. In addition, we need to pass a 4th argument, which is the user session so that the Login link, becomes a Logout link if the user is already logged in - Enlive makes this trivial:

(ns socialsite.templates
  (:use net.cgrand.enlive-html))

(deftemplate page "page.html" [session styles scripts cnt]
  [:link.style]   (clone-for [style styles]
                    (set-attr :href style))
  [:script.import] (clone-for [script scripts]
                    (set-attr :src script))
  [:a#loginout]   (content
                   (if (seq (:nick session))
                     {:tag :a, :attrs {:href "/logout"}, :content ["logout"]}
                     {:tag :a, :attrs {:href "/login"}, :content ["login"]}))
  [:div#content]  (content cnt))

As you can see, deftemplate defines a template with the name 'page' based off the HTML file page.html and takes our 4 arguments which we can use in the body. In the body I use clone-for whenever I need to duplicate an item a certain number of times, set-attr when I need to set attributes and content when Im setting content. Enlive is fairly well documented, but for now all you need to know is that clone-for works exactly like for, so the above says "clone for every style in styles" etc.

If you're new to Enlive you're probably wondering about the way Im setting the login/logout text and its strictly a performance concern. Instead of passing an html-snippet, Im passing Enlives native datastruct, here's how I arrived at it:

socialsite.templates> (html-snippet "<a href="/login">Login</a>")
({:tag :a, :attrs {:href "/login"}, :content ("Login")})

Simply replace the lists with vectors or quoted lists and put that in your templates. With the template in place, we can write our handler:

(defn view-frontpage
  [r]
  (->> (page (:session r) nil nil "Welcome")
       response))

The call to (page) returns the HTML as a seq of strings, passing that to response returns a hashmap which Ring understands. All thats left is defining a route to serve this and launch the server:

(def routes
  (app
   (wrap-file "resources")
   [""] view-frontpage))

(doto (Thread. #(run-jetty #'routes {:port 8080})) .start)

Notice that Im passing the routes argument as a var using #'. This forces Clojure to re-read the value on every read, meaning if I dynamically redef my routes while Im developing the results will show in the browser on the next reload - a must for interactive development. Here's the result:

First screenshot

Looks good - Partly because I used the wrap-file middleware which loads my images/css/scripts etc. You'll notice a big difference from my original Reddit clone in that I have now achieved a 100% separation between code and HTML. Originally I used Hiccup which is a bit like PHP for Clojure in that you seamlessly mix HTML and code. If you want to dig deeper with Enlive, I suggest reading this tutorial.

 

Database

My original clone persisted everything in memory which was conveniet at the time. However with the release of ClojureQL 1.0.0 it seems fitting to work with a real db backend. So here's what you can do if you have MySQL installed:

mysql> create database social;
mysql> grant all privileges on social.* to "social"@"social" identified by "social";
mysql> CREATE TABLE `posts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `upvotes` int(11) NOT NULL DEFAULT '0',
  `downvotes` int(11) NOT NULL DEFAULT '0',
  `submitter` int(11) NOT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
mysql> CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `fullname` varchar(255) NOT NULL,
  `nick` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
mysql> insert into posts (title,url,submitter) VALUES ("ClojureQL 1.0.0 released!", "http://clojureql.org", 1);
mysql> insert into users (fullname,nick,password) VALUES ("Lau Jensen", "Lau", "social");

That will give you a new database called social, a user/pass social/social and two tables posts and users as well as your first user. As a convenience for you guys, I've put this SQL statement in the INSTALL.sql file in the Git repo linked at the bottom. (ps: This is not how you should design a production database!)

Now we need to render the post(s) on the frontpage. In order to do that, we need to join the two tables together and sort them on the amount of upvotes each post has - ClojureQL makes this easy: First we define our connection object and reference our tables

(def db   {:classname   "com.mysql.jdbc.Driver"
           :subprotocol "mysql"
           :user        "social"
           :password    "social"
           :subname     "//localhost:3306/social"})

(def posts (table db :posts))
(def users (table db :users))

Then we simply emit the HTML as before:

(defn view-frontpage
  [r]
  (->> (frontpage @(-> (join posts (project users [:nick])
                             (where (= :posts.submitter :users.id)))
                       (sort [:upvotes#desc])))
       (page (:session r) ["/css/frontpage.css"] nil)
       response))

Now the frontpage snippet gets an ordered list of all the posts, which we just need to inject into our skeleton. First the skeleton:

<body>
  <div class="post">
    <div class="voting">
      <div class="upvote">
        <img src="/images/up.png"/>
      </div>
      <div class="votes"/>
      <div class="downvote">
        <img src="/images/down.png"/>
      </div>
    </div>
    <div class="title"/>
    <div class="submitter"/>
  </div>
</body>

This is quite simple. We have 2 buttons to use for voting a title and a submitter. To fill in these tags we will use another of Enlives features namely the snippet, which allows me to pass a chunk of Enlive-HTML to a template:

(defsnippet frontpage "frontpage.html" [:body :> any-node]
  [posts]
  [:div.post]
  (clone-for [{:keys [id title url nick upvotes downvotes]} posts]
    [:div.votes]     (-> (- upvotes downvotes) str content)
    [:div.title]     (content
                      {:tag :a, :attrs {:href url}, :content [title]})
    [:div.submitter] (-> (str "submitted by " nick) content)))

Let see what we've got:

Second screenshot

Perfect. Now we need to make the voting work, which unfortuntely involves Javascript - Although I like JS it would be nice with a ClojureDSL for that as well:

<script type="text/javascript">
    function vote(id, uri, output) {
        $.ajax({url: uri, type: 'POST', data: {id: id},
                success: function(data) {
                    if (data.substring(0, 2) == "OK") {
                        $(output).html(data.substring(4));
                    } else {
                        Alert("Your vote could not be submitted");
                    }
                }
               });
    }

    function attach_voter(objs, dir) {
        $(objs + ' img').each(function() {
            $(this).click(function() {
                vote($(this).parent().parent().attr("pid"),
                     dir,
                     $(this).parent().parent().find("div.votes"));
            }).css("cursor", "pointer");
        });
    }

    $(function() {
        attach_voter("div.upvote", "/vote/up");
        attach_voter("div.downvote", "/vote/down");
    });
  script>

This is a pretty naive implementation:The function vote calls an URL in the backend, receives a response and updates the votes count with whatever it gets back. The function attach_voter finds all image tags under the select objs and adds an onclick event which simply calls vote. This is clever, because if you only want to allow your users to vote once, simply let the backend keep track of the votes and it will always reply with the correct count.

 

Voting

Now we can add the backend implementation:

(defn vote
  [{id :form-params} direction]
  (let [predicate (where (= :id (get id "id")))]
    (update-in! posts (where predicate)
     (if (= "up" direction)
       {:upvotes   (inc (-> (select posts (where predicate))
                            (pick! :upvotes)))}
       {:downvotes (inc (-> (select posts (where predicate))
                            (pick! :downvotes)))}))
    (-> (format "OK: %s" (-> (select posts (where predicate))
                             (aggregate [["(upvotes - downvotes)" :as :sum]])
                             (pick! :sum)))
        response)))

Basically this function takes the arguments supplied by the JS and a direction in which to vote, ie. up or down. Then it updates the row in posts where predicate is true. If direction is "up" it increments the upvotes, otherwise increments the downvotes. The function pick! is particularily helpful as it derefences our table, checks if theres a single hit and then pulls out the value of the keyword supplied as the argument - this lets us call inc directly on the result. Once thats done, we want to return the score which is simply upvotes minus downvotes - notice how ClojureQL easily lets me slip in a string and alias that.

Now we need moustache to call this function when somebody votes. As you saw in the JS voting is done by POSTing to either /vote/up or /vote/down, here's how to catch that with Moustache:

(def routes
  (app
   (wrap-file "resources")
   [""]               view-frontpage
   ["vote" direction] (wrap-params (delegate vote direction))))

In a production system, I wouldn't do it like this - I would use the same URI for both calls and carry the direction in the POST data. But this lets me show off more of Moustaches syntax and the delegate function - You could use partial, but then you would have to re-arrange the argument order. The ["vote" direction] will match "/vote/*" where * can be anything except another slash - whatever is passed goes into the var direction.

 

Submitting posts

Now we need to allow users to submit posts, which follows the outline above:

Since the first 2 are exceedingly trivial, I'll simply demonstrate the handler:

(defn accept-submission
  [req]
  (let [params (-> req :form-params)
        user   1];(-> req :session :id)]
    (conj! posts {:title     (params "title")
                  :url       (params "url")
                  :submitter user})
    (response "OK")))

Because I haven't written the 'login' functionality yet, I've hardcoded the user ID to 1. Then I simply conj! the hasmap containing the values from the post onto posts and reply "OK". Normally, you would want to filter/sanitize your results and add some error handling but thats beyond the scope of this post. Next you might want to group both the POST and GET replies on the URI "/submit", here's how to do it:

(def routes
  (app
   (wrap-file "resources")
   [""]               view-frontpage
   ["submit"]         {:get  view-submission-page
                       :post (wrap-params accept-submission)}
   ["vote" direction] (wrap-params (delegate vote direction))))

Passing Moustache a map, instead of a function lets you group related functionality on the same URI.

 

Logging in/out

All we need now is to give users the ability to log in and out. In order to do that, we need to perform the usual steps ending with the handler:

(defn authenticate-user
  [{params :form-params}]
  (let [user  @(select users (where (and (= :nick     (params "usr"))
                                         (= :password (params "psw")))))]
    (if (= 1 (count user))
      (assoc (redirect "/") :session (first user))
      (redirect "/login?err=true"))))

Ring has a middleware called wrap-session, which when enabled keeps a :session key in every request which you can use to store information about each session. Here I simply select the row from the database which has the correct username/password combination and if I get a hit then I associate that record with the session - If I dont get a hit, I redirect the client to the login page and show an error. This brings us to the final piece of the puzzle...

 

Middlewares

One of the great delights of working with Ring is its middlewares. They act as functions which work on each request and then passes the request down the line, possibly modified. One middleware which we definitely need now, is a security middleware which rejects unauthorized users from pages they're not allowed to see, here's a naive way to achieve that:

(defn wrap-security
  [app]
  (fn [req]
    (let [uri (:uri req)]
      (if (or (-> req :session :nick seq)
              (= uri "/")
              (= uri "/login"))
        (app req)
        (redirect "/?err=Not logged in")))))

If the user is logged in or is trying to access one of the two public URIs the application handles the request as normally, but if not it redirects to the frontpage. The entire middleware stack for this app looks like so:

(def routes
  (app
   (wrap-file   "resources")
   (wrap-reload '[socialsite.templates])
   (wrap-session)
   (wrap-security)

   [""]               view-frontpage
   ["logout"]         logout
   ["login"]          {:get view-login-page
                       :post (wrap-params authenticate-user)}
   ["submit"]         {:get  view-submission-page
                       :post (wrap-params accept-submission)}
   ["vote" direction] (wrap-params (delegate vote direction))))

wrap-file allows me to serve static files from the resources directory, wrap-reload reloads the Enlive templates on every pageview, very handy for development, very no-no for production. The final two I've already commented on - Lets see what we ended up with:

Third screenshot

 

There you go, your own Clojure Driven Social Media website to help keep the community engaged and connected :)

The source code for this app is on Github, to launch it simply follow these simple steps:

$ git clone http://github.com/LauJensen/SocialSite.git
$ cd SocialSite
$ mysql -u root -p < INSTALL.sql 
Enter password: *******
$ cake repl
user=> (use 'socialsite.core)
nil

The INSTALL.sql file will create the social/social user/pass on your local MySQL installation as well as the social database which is populates with two tables. Once you issue the use command in the repl, you can access the site on http://127.0.0.1:8080 - Here you can login with my user "Lau" / "social" - Enjoy!


View the RSS feed Follow Lau on Twitter

About the author:

Lau Jensen is the founder and owner of Best In Class a danish consultancy company which specializes in Clojure development.

Lau is also one of the instructors driving the Conj Labs initiative. If you would like to be notified once new blogposts are published, you can follow Lau on Twitter.

Ben
2011-01-10 03:22:08
Hi! 

Just wanted to say 'hi' and thank you for your work (blog posts, screencasts) ; these are very inspiring contributions! 
Scott
2011-01-10 04:16:40
For those interested in mixing clojure, html, js, and css in a way that takes the shame of hiccup/php to a new level see:
http://pastie.org/1444730
Philipp
2011-01-10 11:05:17
Great post, thanks!

"Now we need to make the voting work, which unfortuntely involves Javascript - Although I like JS it would be nice with a ClojureDSL for that as well"

You might want to check out scriptjure: https://github.com/arohner/scriptjure

It lets you create JS from Clojure. Pretty easy to use - at least for the examples I've tried.

Regards,
Philipp
faenvie
2011-01-11 05:19:22
i made my own build-script for socialsite using gradle/clojuresque and noticed, that org.clojure:clojure:1.3.0-alpha4 is resolved.

i think, that this is because of enlive and moustache both define the clojure dependency like this:

<dependency>
  <groupId>org.clojure</groupId>
  <artifactId>clojure</artifactId>
  <version>[1.1.0,)</version>
</dependency>
  
which probably means: resolve against latest release of clojure.

it is not clear to me, why 'lein deps' resolves against org.clojure:clojure:1.2.0 

btw.: maven is a mess all over and lein will never make it because of that. 
helmut denk
2011-01-11 12:35:40
thanks for this inspiring blogpost. 

note: tomcat-people may not get happy with deploying the resulting war as non-ROOT.

>>it is not clear to me, why 'lein deps' resolves against org.clojure:clojure:1.2.0 

probably because lein/maven is configured to recognize the alpha in revision-id and classify alpha-release as different from release.
Anata
2011-01-13 09:16:10
i got it to run as ROOT.war in tomcat with one line changed: 

(wrap-file   "/appserver/tomcat/webapps/ROOT")
;(wrap-file   "resources")

is there anybody, who can give me a hint, how to deploy it to tomcat as socialsite.war ? 
Lau
2011-01-13 09:19:02
Thanks to everybody for your comments.

To deploy this on Tomcat you need to raise every uri one level by adding a wrapping (app "socialsite" (app ...)). Then I typically put an NGINX in front of Tomcat which does a proxy_pass to that uri.
Helmut Denk
2011-02-07 10:58:17
here is a gist that may be helpful for tomcat-deployers: 

http://gist.github.com/815806