Applying math functions to user input via text-area with hiccup and compojure POST method in defroutes

68 views
Skip to first unread message

Jesse Diaz

unread,
Feb 25, 2014, 12:02:50 PM2/25/14
to comp...@googlegroups.com
I asked this question here on stack overflow, but am not getting very many satisfactory responses:
https://stackoverflow.com/questions/22003524/clojure-compojure-do-you-need-to-use-a-database-to-process-user-input-on-a-webf?noredirect=1#comment33365047_22003524

In nutshell, I have set up a landing page for my web-app where I want the user to input vectors of student grades (and corresponding weights) to be processed into finals. The page loads fine, but when I hit the process button, I am invariably routed to a 404 html page that is specified in my defroutes, meaning that it skips over the POST "/" method, altogether. The things that I cannot figure out are how to:

1) bind user input to the variables that I want to pass into my functions
2) call my functions in such a way as to have them rendered in the browser as a copy/pastable list of finals, and
3) make the button on the landing page route properly to the POST method in defroutes.

Please have a look at the link above and give me any advice you can. I have been following the web development with Clojure book at Prag Progs by Yogthos, and the guestbook tutorial there seems to use a database to store user input to be called and rendered later. I would like to avoid using a database for this simple application though, and the database information in that book seems a little outdated. Also, I used the lein new heroku command to create a skeleton app for my project. Eventually I intend to deploy it to Heroku, so I thought it would be convenient. I am having trouble understanding the dynamic development process though with that template and am still restarting the server, waiting 30 seconds every time I make a change to the source code. If someone could tell me how I can see my changes using the refresh button in my browser, I would really appreciate that as well.

James Reeves

unread,
Feb 25, 2014, 12:37:02 PM2/25/14
to Compojure
Hi Jesse,

I pasted the code you provided on Stack Overflow in a new project. Pressing the submit button does not skip over the POST route. There's likely something wrong with the code you're working on that's not in the code you provided on Stack Overflow.

There are a few odd things in your code. You place the text areas outside the form, so they'll never be passed as post parameters. You don't apply any middleware to your app route. You don't have any mechanism in place to read the weights or heights. You have no top-level <html> or <body> tags. The response map in the "/landing" route is unnecessary. The "route/not-found" function returns a route already, so there's no need to wrap it in another route.

Regarding dynamic development, maybe the easiest way is to use Lein-Ring. That will add automatic reloading to your app.

- James


--
You received this message because you are subscribed to the Google Groups "Compojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to compojure+...@googlegroups.com.
To post to this group, send email to comp...@googlegroups.com.
Visit this group at http://groups.google.com/group/compojure.
For more options, visit https://groups.google.com/groups/opt_out.

Jesse Diaz

unread,
Feb 25, 2014, 9:31:27 PM2/25/14
to comp...@googlegroups.com, ja...@booleanknot.com
Thank you very much James. You pretty much schooled me with your response. I am taking my first baby step into the world of computers and am humbled by the awesome support and enthusiasm in the Clojure community. The learning curve is kind of steep compared to the other languages I've taken a look at but it is overwhelmingly more fun and rewarding for the effort put in. I am very excited to see how the language, and my ability (fingers crossed), will develop in the next few years.

Jesse

Jesse Diaz

unread,
Feb 26, 2014, 12:28:01 AM2/26/14
to comp...@googlegroups.com, ja...@booleanknot.com
Dear James,

I just looked at my project again and thought I should mention that I have another clojuregrade.web namespace through which I require the clojuregrade.landing namespace (as landing). I then call the landing/home function through a defroutes action defined in clojuregrade.web. I am using the web.clj file because this is how the lein new heroku command templates the project. I will post the code from that file here with comments. I must admit that I am still trying to figure out how middleware works, so if that is the reason for my continuing 404 error then I would really appreciate any pointers you are willing to give to help me grok that. The official docs are a little confusing for me. In any case, I'll keep researching that now and when I level up I'll be sure to pass the good karma along.

web.clj file:
 
(ns clojuregrade.web
  (:use ring.util.response)
  (:require [compojure.core :refer [defroutes GET PUT POST DELETE ANY]]
            [compojure.handler :refer [site]]
            [compojure.route :as route]
            [clojure.java.io :as io]
            [ring.middleware.stacktrace :as trace]
            [ring.middleware.session :as session]
            [ring.middleware.session.cookie :as cookie]
            [ring.adapter.jetty :as jetty]
            [ring.middleware.basic-authentication :as basic]
            [cemerick.drawbridge :as drawbridge]
            [environ.core :refer [env]]
            [clojuregrade.landing :as landing])) ; <- requiring the external file

(defn- authenticated? [user pass]
  ;; TODO: heroku config:add REPL_USER=[...] REPL_PASSWORD=[...]
  (= [user pass] [(env :repl-user false) (env :repl-password false)]))

(def ^:private drawbridge
  (-> (drawbridge/ring-handler)
      (session/wrap-session)
      (basic/wrap-basic-authentication authenticated?)))

(defroutes app
  (ANY "/repl" {:as req}
       (drawbridge req))            ;; <- Not sure I really need all this drawbridge/auth stuff or if it is causing a problem by hanging out in my code.
  (GET "/" [] (landing/home))  ;; <- callling the home function from clojuregrade.landing
  (ANY "*" []
       (route/not-found (slurp (io/resource "404.html")))))

(defn wrap-error-page [handler]
  (fn [req]
    (try (handler req)
         (catch Exception e
           {:status 500
            :headers {"Content-Type" "text/html"}
            :body (slurp (io/resource "500.html"))}))))

(defn -main [& [port]]
  (let [port (Integer. (or port (env :port) 5000))
        ;; TODO: heroku config:add SESSION_SECRET=$RANDOM_16_CHARS
        store (cookie/cookie-store {:key (env :session-secret)})]
    (jetty/run-jetty (-> #'app
                         ((if (env :production)
                            wrap-error-page
                            trace/wrap-stacktrace))
                         (site {:session {:store store}}))
                     {:port port :join? false})))

;; For interactive development:  
;; (.stop server)                 ;; <- can't find any documentation for how to use this
;; (def server (-main))       ;; <- or this. When I un-comment them I get var not found and null pointer exceptions.


Here is my current clojuregrade.landing namespace:

(ns clojuregrade.landing
  (:require [compojure.core :refer [defroutes GET PUT POST DELETE ANY]]
            [compojure.handler :refer [site]]
            [compojure.route :as route]
            [clojure.java.io :as io]
            [ring.middleware.stacktrace :as trace]
            [ring.middleware.session :as session]
            [ring.middleware.session.cookie :as cookie]
            [ring.adapter.jetty :as jetty]
            [hiccup.core :refer :all]
            [hiccup.form :refer :all]
            [hiccup.element :refer :all]))
           

;; these functions are used for the process-grades function, which I want to call after having the form-to POST  "/" input bound in and passed from the home function.
;; there may be some subtle mistake here that is causing trouble, but I have tested this code pretty thoroughly.
(defn percentify
  "adjust raw score to percentile"
  [raw percentile]
  (* (/ raw 100) percentile))

(defn percentify-vector
  "maps a vector of percentile adjustments to a vector of grades"
  [weights grades]
  (map percentify grades weights))

(defn augment-vectors
  "augments all the elements in all of the vectors in a grades list into their corresponding weighted values"
  [weights grades]
  (mapv (partial percentify-vector weights) grades))


;; I want to take the weights and grades variables from the home function when the user submits the form, and use them in the function below.
;; If I try to use the (read-string weights) (read-string grades) substitues for weights and grades, the function fails in the repl, but without them it works as a stand alone function
;; over lists of vectors. I have a feeling that this is where the key issue lies.

(defn process-grades
  "Takes user input from home's form-to function and processes it into the final grades list"
  [weights grades]
(->> grades
     (map (partial percentify-vector weights))
     (mapv #(apply + %))))

;; I updated this.
(defn home [& [weights grades error]]
  (html5
    [:head
    [:title "Home | Clojuregrade"]]
    [:body
    [:h1 "Welcome to Clojuregrade"]
    [:p error]
    [:hr]
   (form-to [:post "/"]
    [:h3 "Enter the weights for each of the grades below. Of course, all of the numbers should add up to 100%. Be sure to include the brackets"
    [:br]
     (text-area {:cols 30 :placeholder "[40 10 50] <- adds up to 100%"} "weights" weights)]
    [:h3 "Enter ALL of the grades for EACH STUDENT in your class.
      Make sure that each of the grades is ordered such that the grade corresponds
      to its matching weight above."
    [:br]
    (text-area {:rows 15 :cols 30 :placeholder
"[89 78 63]
                    [78 91 60]
                    [87 65 79]
                    ...                   
                   
                                                         (Each grade corresponds to one of the weights above, so order is important. You can copy and paste directly from your excel file but don't forget the brackets!)" } "grades" grades)]
     (submit-button "process"))]))

 (defn processed [weights grades]
  (cond
   (empty? weights)
   (home weights grades "You forgot to add the weights!")
   (empty? grades)
   (home weights grades "You forgot to add the grades!")
  :else
  (do
   (html 
    [:h2 "These are your final grades."]
    [:hr]
    [:p (process-grades (read-string weights) (read-string grades))]))))

(defroutes app
  (GET "/landing" []
       {:status 200
        :headers {"Content-Type" "text/html"}
        :body (home)})
  (POST "/" [weights grades] (processed weights grades))
  (ANY "*" []
       (route/not-found (slurp (io/resource "404.html")))))

(defn wrap-error-page [handler]
  (fn [req]
    (try (handler req)
         (catch Exception e
           {:status 500
            :headers {"Content-Type" "text/html"}
            :body (slurp (io/resource "500.html"))}))))

James Reeves

unread,
Feb 26, 2014, 6:56:54 AM2/26/14
to Compojure
Hi Jesse,

You have two sets of routes in your application:

clojuregrade.web/app
- clojuregrade.landing/app

Only the first one is passed to run-server, but only the second one has your POST route. The reason your app isn't seeing your POST route is simply that you're not giving it to your web server. You're defining it, then doing nothing with it.

Regarding interactive development, there are a few libraries, like Lein-Ring, that will handle it for you. However, it might be worth going over it from first principles.

The simplest way to start a web server in Ring is:

  (run-jetty app {:port 3000})

However, if we run this in a REPL, it will block execution. Ideally we want the web server to run in the background.

  (run-jetty app {:port 3000, :join? false})

Now we have a web server running in the background, but what happens if we want to redefine "app"? Because we're passing it in as a value, redefining "app" will have no effect on the running server.

  user=> (defn app [req] {:status 200, :headers {}, :body "Foo"})
  #'user/app
  user=> (use 'ring.adapter.jetty)
  nil
  user=> (run-jetty app {:port 3000, :join? false})
  #<Server org.eclipse.jetty.server.Server@2c4689ad> 
  user=> (defn app [req] {:status 200, :headers {}, :body "Bar"})
  #'user/app

In the above case, if we access the web server running on port 3000, it will return "Foo" and not "Bar".

To fix this, we can pass "app" as a var. Vars can be executed like functions, but will always perform a lookup. This makes them somewhat slower, but more useful for development.

If we restart the REPL and use a var instead:

  user=> (defn app [req] {:status 200, :headers {}, :body "Foo"})
  #'user/app
  user=> (use 'ring.adapter.jetty)
  nil
  user=> (run-jetty #'app {:port 3000, :join? false})
  #<Server org.eclipse.jetty.server.Server@2c4689ad> 
  user=> (defn app [req] {:status 200, :headers {}, :body "Bar"})
  #'user/app

Then the web server will return "Bar" instead. This allows us to reload namespaces, and have changes in "app" show up in our web server.

We can go a little further. Perhaps we sometimes want to stop the running web server without restarting the REPL. To do this, we can capture the return value of run-jetty:

  user=> (def server (run-jetty #'app {:port 3000, :join? false}))
  #'user/server

Now to stop it:

  user=> (.stop server)

Typing this out all the time could get tiresome, so lets automate the above process:

  (ns user
    (:require [ring.adapter.jetty :refer (run-jetty)]
              [clojuregrade.web :refer (app)]))

  (def server nil)

  (defn start []
    (alter-var-root
     #'server
     (constantly (run-jetty #'app {:port 3000, :join? false})))

  (defn stop []
    (when server (.stop server)))

We can put this file directly in our source directory "/src/user.clj", and it will be loaded by default in our REPL. Then we can type (start) to start the server, and (stop) to stop the server.

In order to reload our application, we can use:

  user=> (require 'clojuregrade.web :reload-all)

Or, if we want files to be loaded automatically when files are modified, we can add the Ring wrap-reload middleware to the run-jetty call:

  (run-jetty (wrap-reload #'app) {:port 3000, :join? false}))

- James

Sven Richter

unread,
Feb 26, 2014, 7:26:27 AM2/26/14
to comp...@googlegroups.com
Hi Jesse,

I uploaded a working example to https://github.com/sveri/grades
Please study this carefully, its working and showing how it could be done. However, you really have to take the time and read some tutorials. Like I said, a small portion of HTML and luminusweb.net are a good start.

Best Regards,
Sven

PS: please accept my answer at SO as it solves the question you asked there.

Jesse Diaz

unread,
Feb 26, 2014, 10:38:50 AM2/26/14
to comp...@googlegroups.com
Thanks James,

Your explanation about dynamic development was very thorough and clear. I understand what you said about how the clojuregrade.web defroutes function was missing the POST "/". I'm now rereading all the documentation and things are starting to come together. The last thing I am having trouble with is augmenting the grades being passed from home into the proper datastructure so that it can be used in the process-grades function without throwing an exception. Apparently read-string isn't doing the trick. I will also look more up about this on my own.

Sven, you were also very helpful. An upvote is coming your way ;)

Jesse

Reply all
Reply to author
Forward
0 new messages