Task E: Deleting Pictures

It’s reasonable for users to want to remove images they no longer wish to display. We need to provide a way for users to select images they wish to remove and tell the application about it. To delete a picture, we’ll do the following:

  • Delete the image

  • Delete the thumbnail

  • Delete the database entry for the image

Since the images can be removed only by the owner, we’ll check if the page matches the user in the session. When this is the case, we’ll allow the user to mark pictures he wishes to delete and submit his selection using the Delete button.

So far, we’ve only been creating static pages. Let’s look at how to add some client-side interaction using Ajax. In this section, we’ll include JavaScript in our page, call our handler using an HTTP POST from the browser, and return a JavaScript Object Notation response to the client.

We’ll use an Ajax call to notify the server of the images to be deleted and update the page to reflect the result of the operation.

First, let’s create a function in the picture-gallery.models.db namespace to delete the image from the database.

picture-gallery-e/src/picture_gallery/models/db.clj
 
(​defn​ delete-image [userid ​name​]
 
(with-db
 
sql/delete-rows :images [​"userid=? and name=?"​ userid ​name​]))

Then we’ll add a function in the picture-gallery.routes.upload namespace to perform the three deletion tasks we outlined. We need to provide the function with the user ID and the image name to accomplish its task.

We’ll wrap the actions in a try/catch block. If the deletion is successful, we’ll return ok. If we encounter any errors, we’ll return the error message instead.

picture-gallery-e/src/picture_gallery/routes/upload.clj
 
(​defn​ delete-image [userid ​name​]
 
(​try
 
(db/delete-image userid ​name​)
 
(io/delete-file (​str​ (gallery-path) File/separator ​name​))
 
(io/delete-file (​str​ (gallery-path) File/separator thumb-prefix ​name​))
 
"ok"
 
(​catch​ Exception ex (​.​getMessage ex))))

Next, we’ll add a handler to handle the deletion of multiple images, along with its route.

picture-gallery-e/src/picture_gallery/routes/upload.clj
 
(​defn​ delete-images [names]
 
(​let​ [userid (session/get :user)]
 
(resp/json
 
(​for​ [​name​ names] {:name ​name​ :status (delete-image userid ​name​)}))))
 
 
(defroutes upload-routes
 
(GET ​"/img/:user-id/:file-name"​ [user-id file-name]
 
(serve-file user-id file-name))
 
 
(GET ​"/upload"​ [info] (restricted (upload-page info)))
 
 
(POST ​"/upload"​ [file] (restricted (handle-upload file)))
 
 
(POST ​"/delete"​ [names] (restricted (delete-images names))))

There, delete-images accepts a list of names for the images to be deleted. We then grab the user ID from the session, call delete-image for each image name, and return the outcome of each operation to the client.

Since our plan is to be able to select multiple thumbnails and call the /delete route using Ajax, we need to add some JavaScript to our page. We’ll create a new file under resources/public/js called gallery.js. Our gallery page will load this file, which will provide the client-side functions for managing the gallery.

Let’s write a function to select some images and make the Ajax call. We’ll be using jQuery to help with our JavaScript, so let’s include it in our base layout. This necessitates referencing include-js from hiccup.page in the namespace declaration.

 
(​ns​ picture-gallery.views.layout
 
(:require ​..​​.
 
[hiccup.page :refer [html5 include-css include-js]]))
 
 
..​​.
 
 
(​defn​ base [& content]
 
(html5
 
[:head
 
[:title ​"Welcome to picture-gallery"​]
 
(include-css ​"/css/screen.css"​)
 
(include-js ​"//code.jquery.com/jquery-2.0.2.min.js"​)]
 
[:body content]))

We’re finally ready to write our function to delete the images on the client side.

 
function deleteImages() {
 
var​ selectedInputs ​=​ $(​"input:checked"​)​;
 
var​ selectedIds ​=​ []​;
 
selectedInputs
 
.​each(function() {
 
selectedIds.push($(this)​.​attr('id'))​;
 
})​;
 
 
if​ (selectedIds.length ​<​ 1) alert(​"no images selected"​)​;
 
else
 
$.post(​"/delete"​,
 
{names: selectedIds},
 
function(response) {
 
var​ errors ​=​ $('<ul>')​;
 
$.each(response, function() {
 
if​(​"ok"​ ​=​​=​​=​ this.status) {
 
var​ element ​=​ document.getElementById(this.name)​;
 
$(element)​.​parent()​.​parent()​.​​remove​()​;
 
}
 
else
 
errors
 
.​append($('<li>',
 
{html: ​"failed to remove "​ ​+
 
this.name ​+
 
": "​ ​+
 
this.status}))​;
 
})​;
 
 
if​ (errors.length ​>​ 0)
 
$('#error')​.​​empty​()​.​append(errors)​;
 
},
 
"json"​)​;
 
}

In that code, we select checked inputs and then grab the ID attribute for each of them. Next we make an HTTP POST and pass those IDs to our delete-images handler on the server.

The server returns a list of update statuses. When the update is successful, the status is set to ok and we delete the corresponding element. Otherwise, we create an error message based on the status and display it to the user.

The JavaScript file needs to be referenced on the page in order to run. We can do this using the include-js the same way we did with jQuery in our layout. Since the JavaScript is specific to the gallery page, we’ll add it directly in the route declaration.

 
(:require ​..​​.​ [hiccup.page :refer :all])
picture-gallery-e/src/picture_gallery/routes/gallery.clj
 
(defroutes gallery-routes
 
(GET ​"/gallery/:userid"​ [userid]
 
(layout/common
 
(include-js ​"/js/gallery.js"​)
 
(display-gallery userid))))

We’ll also require a couple of changes in the way we render our thumbnails, as currently there are no check boxes associated with them for the user to check. Let’s add the necessary references to our gallery namespace and update the thumbnail-link as follows:

 
(:require ​..​​.
 
[hiccup.form :refer [check-box]])
picture-gallery-e/src/picture_gallery/routes/gallery.clj
 
(​defn​ thumbnail-link [{:keys [userid ​name​]}]
 
[:div.thumbnail
 
[:a {:class ​name​ :href (image-uri userid ​name​)}
 
(image (thumb-uri userid ​name​))
 
(​if​ (​=​ userid (session/get :user)) (check-box ​name​))]])

Now, if the userid matches the one in the session, we’ll also render a check box along with the name of the image in our thumbnail div (see the following figure).

images/screenshots/gallery_checkbox.png

Figure 25. Gallery check box

The check box is there, but we can’t interact with it yet. To do that we’ll update display-gallery to provide a Delete button and a div to display the errors.

picture-gallery-e/src/picture_gallery/routes/gallery.clj
 
(​defn​ display-gallery [userid]
 
(​if-let​ [gallery (​not-empty​ (​map​ thumbnail-link (db/images-by-user userid)))]
 
[:div
 
[:div#error]
 
gallery
 
(​if​ (​=​ userid (session/get :user))
 
[:input#delete {:type ​"submit"​ :value ​"delete images"​}])]
 
[:p ​"The user "​ userid ​" does not have any galleries"​]))

Now, if we have a gallery to display we’ll also provide a Delete button when the user is the owner of the gallery. We’ll bind the delete function to the button in our gallery.js when the page loads.

picture-gallery-e/resources/public/js/gallery.js
 
$(document).ready(​function​(){
 
$(​"#delete"​).click(deleteImages);
 
});

We should now be able to test and see that each thumbnail has a check box when the owner of the gallery views the gallery page. If we select a few images and press the Delete button, they disappear from the page. We can also check that the images and the thumbnails are correctly deleted on disk and in the database.

Ajax and the Servlet Context

The preceding code will work fine when the application runs standalone. However, if we ran our application on an application server, the Ajax request would fail because the full URL would need to have the application context prefixed.

Unfortunately for us, the browser is not aware that our application has a context. One way we can get around this problem is to populate a variable on the page before we serve it.

The request map contains a key with the name :context. The value of this key is exactly what we’re looking for. This might appear to be a bit of a conundrum. After all, we don’t wish to have to pass the request explicitly to all our handlers just so we can grab the context from it.

Luckily, Compojure uses the compojure.response.Renderable protocol to convert what the handler returns into a Ring response. This protocol looks like this:

 
(​defprotocol​ Renderable
 
(render [this request]
 
"Render the object into a form suitable for the given request map."​))

As you can see, the protocol defines a single method called render. This method accepts the object instance and the request that we’re after.

To use this protocol, we’ll first need to add a reference to it and the ring.util.response/response to our picture-gallery.views.layout namespace declaration:

picture-gallery-e/src/picture_gallery/views/layout.clj
 
(​ns​ picture-gallery.views.layout
 
(:require [hiccup.page :refer [html5 include-css]]
 
[hiccup.element :refer [link-to]]
 
[noir.session :as session]
 
[hiccup.form :refer :all]
 
[hiccup.page :refer [include-css include-js]]
 
[ring.util.response :refer [content-type response]]
 
[compojure.response :refer [Renderable]]))

Since we’re implementing the protocol, we’ll need to set the appropriate response headers manually. To do that we’ll create the utf-8-response function to set the content type to text/html and the encoding to UTF-8.

picture-gallery-e/src/picture_gallery/views/layout.clj
 
(​defn​ utf-8-response [html]
 
(content-type (response html) ​"text/html; charset=utf-8"​))

Next, we’ll create a RenderablePage type that will extend the Renderable protocol. We’ll move the code from our base layout function to the render method.

Since we now have access to the request, we can add a JavaScript variable to the head section of our page with the value of the context.

Lastly, the body of the render method will have to be wrapped in the response function we included earlier. The final result is as follows:

picture-gallery-e/src/picture_gallery/views/layout.clj
 
(​deftype​ RenderablePage [content]
 
Renderable
 
(render [this request]
 
(utf-8-response
 
(html5
 
[:head
 
[:title ​"Welcome to picture-gallery"​]
 
(include-css ​"/css/screen.css"​)
 
[:script {:type ​"text/javascript"​}
 
(​str​ ​"var context=""​ (:context request) ​"";"​)]
 
(include-js ​"//code.jquery.com/jquery-2.0.2.min.js"​)]
 
[:body content]))))

The base layout function will now return an instance of the RenderablePage instead of generating the layout:

picture-gallery-e/src/picture_gallery/views/layout.clj
 
(​defn​ base [& content]
 
(RenderablePage. content))

Finally, we’ll update our JavaScript to prepend the variable to the URL when making the POST request.

picture-gallery-e/resources/public/js/gallery.js
 
$.post(context + ​"/delete"​,
 
{names: selectedIds},
 
function​(response) {
 
var​ errors = $(​'<ul>'​);
 
$.each(response, ​function​() {
 
if​(​"ok"​ === this.status) {
 
var​ element = document.getElementById(this.name);
 
$(element).parent().parent().remove();
 
}
 
else
 
errors
 
.append($(​'<li>'​,
 
{html: ​"failed to remove "​ +
 
this.name +
 
": "​ +
 
this.status}));
 
});
 
if​ (errors.length > 0)
 
$(​'#error'​).empty().append(errors);
 
},
 
"json"​);

Now the context will be prepended to the URL. When the context is not available, the variable will contain a blank string and the request will work exactly as it did before.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.242.141