User Tools

Site Tools



My web application met many of the project objectives: users can register an account, log in with their credentials and upload pictures. In addition, they can add comments to their pictures, view when their pictures have been submitted and delete certain pictures or their accounts. I also fell short of some of the objectives: commenting on photos, implementing a real-time chat service and enabling personal messaging. In the paragraphs to follow I will give a brief description of the project workflow, important milestones and notable roadblocks.

User Registration

The first workflow is account registration. An independent workflow is categorized as a namespace where all functions that handle a particular tasks are stored. As a result, I created a page to collect user's input such as username and password and validate it. The two important routes for authentication are GET for rendering the page and POST for handling form submissions. The input validation consists of checking that a username was entered and the entered passwords match. A link to the registration page is added so users can access it.

Storing in Database

User information is stored in the database when an account is created. The noir.util.crypt namespace is used to hash passwords before storing them. A page controller is implemented to check if a user with the same ID is already in the database. We read users based on their IDs using the with-query-results macro, getting records and returning the first instance. The way I allow users to log in is a handler which accepts the user's ID and password, authenticates them to the ones stored in the database and either grants or denies the request.

(defn handle-login [id pass]
  (let [user (db/get-user id)] ;;accept ID and password
    (if (and user (crypt/compare pass (:pass user))) ;;compare to what's stored in the DB using crypt
      (session/put! :user id))) ;;put them in session if check is successful 
    (resp/redirect "/")) 

In order to expose the functionality to the client, every route needs to the routes of the respective namespace, in this case auth-routes. Example of namespace routes:

(defroutes auth-routes
  (POST "/login" [id pass]
        (handle-login id pass))) ;;use function handle-login to validate form upon form submission 

Uploading Pictures and Comments

Clojure piggy-backs on Java's libraries when it comes to scaling images, namely using the java.awt.geom package. I created a function rendering the upload page and saving files by the user. A galleries directories is used to store the files:

(defn gallery-path[] 

A new route and handler are needed to properly display an image:

(defn serve-file [file-name]
  (file-response (str (gallery-path) File/separator file-name)))

Instead of displaying a full-size photo at once, I decided to generate a thumb-nail when a file is uploaded. A function scales and stores the thumbnail file and a separate function stores the full-size image. I ensure that a path is created during user registration:

  (defn gallery-path []
    (let [user-path (File. (gallery-path))]
      (if-not (.exists user-path) (.mkdirs user-path)) ;;check if a user-path exists, create a new directory if it doesn't
      (str (.getAbsolutePath user-path) File/separator)))

Adding Descriptions and Timestamps

In order to allow for image descriptions I had to alter the database by adding a description column: In addition, I had to make amendment to the add-image function which inserts records every time an image is uploaded:

(defn add-image [userid name message]
    (if (sql/with-query-results
          ["select userid from images where userid = ? and name = ? and description = ?" userid name message]
          (empty? res))
      (sql/insert-record :images {:userid userid :name name :description message})
        (Exception. "You have already uploaded an image with the same name.")))))

Additionally, I created a read-timestamps function which selects the date a picture has been uploaded. Then, I used this function to display the timestamp along with the picture and its description as follows:

(defn thumbnail-link [{:keys [userid name]}]
   [:div.time (format-time (get (into (sorted-map) (db/read-timestamps userid name)) :timestamp))] ;;format time in mm/dd/yyyy format
   [:a {:class name :href (image-uri userid name)}
    (image (thumb-uri userid name))]
    [:div.desc (get (into (sorted-map) (db/descriptions-by-user userid name)) :description)]
    (if (= userid (session/get :user))
      (check-box name))])

Restricting Access

lib-noir provides functionality to specify rules for page restriction. Thus, a user must be logged in in order to access any of the site's content (except for user registration). The session is checked to see if a user is in session:

(defn user-page [_]
  (session/get :user))

Then we access the specified rules like this:

(def app (noir-middleware/app-handler
  :access-rules [user-page]]))

Then using the restricted macro, we can restrict access to pages:

(defroutes upload-routes
  (GET "/upload" [info] (restricted (upload-page info)))) ;;not logged-in users cannot view uploaded files

Code Refactoring

Some of the common code used across pages can be reused. All functions used in multiple locations are stored in the util namespace. Some of them include thumb-prefix, thumb-uri and gallery-path for separating thumbnails from full-size images, specifying the thumbnail uri and the gallery path respectively.

Deleting Pictures

When deleting a picture, we also have to delete all data associated with it: the image itself, its thumbnail and the db entry for the image. To delete an image from the db I use the following function in the db namespace:

(defn delete-image [userid name]
    sql/delete-rows :images ["userid=? and name=?" userid name]))

Then I add a function to perform all three deletions:

(defn delete-image [userid name]
       (db/delete-image userid name) ;;delete from database
       (io/delete-file (str (gallery-path) File/separator name)) ;;delete physical file
       (io/delete-file (str (gallery-path) File/separator thumb-prefix name)) ;;delete thumbnail of physical file
       (catch Exception ex 
         (error ex "an error has occured while deleting" name)
         (.getMessage ex))))

In order to allow users to delete multiple images at once, I used a JavaScript function to select images and make Ajax call. I also check if the user is the owner of the gallery and provide a 'Delete' button if they are:

(defn display-gallery [userid]
  (if (= userid (session/get :user))
       [:input#delete {:type "submit" :value "delete images"}])

Finally, I use the compojure.response.Renderable protocol to convert the result returned by the handler into a Ring response in order to run the application on an application server. Otherwise, the Ajax request would fail.

Delete Account

All user-related information is deleted when a user deletes their account. Building on the delete-image function from the previous section, I select all images selected by the user, remove the account from the users table and delete the user folder. Since only the user in session can delete their account, the route to the page is restricted.

(defn delete-account-page []
    (form-to [:post "/confirm-delete"] ;;delete account
             (submit-button "delete account"))  
    (form-to [:get "/"] ;;back out if user has chosen to delete account by accident
             (submit-button "cancel")))) 


The biggest hurdle of this project is creating environment variables in order to set different profiles and deploy the app on version control. The main issue associated with environment variables is the fact that the Counterclockwise plug-in doesn't load up variables from the profile and I had to use Environ - a library that manages env variables - to read them directly. From my understanding, Environ automatically coerces a variable like this “DB_URL” into the more idiomatic :db-url so all environment variables need to be uppercased and underscored. After making the appropriate edits, however, I got the following error:

db-spec {:password nil, :subprotocol "postgresql", :user nil, :subname nil} is missing a required parameter

I still haven't figured out what the missing required parameter is. The problem with environment variable is that without them deployment is simply not possible.

The other major issue is creating a live chat service where users will be able to communicate with one another in real time. There are many web socket services for Clojure including Aleph and http-kit, both asynchronous Clojure libraries. However, I experienced difficulties with synchronizing the chat server with the picture-gallery server. The best I could come up with was a kludge of two separate applications running on two separate ports. Both applications have to be running at the same time in order for them to function properly. A batch file can be written to start up both programs simultaneously.

Bottom Line

This independent study showed me the importance and relevance of functional languages in the web development space. The clojure/compojure duo is powerful in creating a multifaceted application (registration, login, file upload, deletion). I learned how to separate workflows in individual namespaces, use libraries to accomplish certain tasks and build a robust db-powered application. I will leverage the foundation I created in this independent study to resolve the roadblocks I ran into and incorporate new features into the picture-gallery.

Screen Shots

Home Page Upload Images Galleries Example Gallery Delete Account

cs444asa/ctv.txt · Last modified: 2016/05/06 20:12 by nasko