The purpose of this assignment is to learn how users of your website can upload files and later view them on your website. You will learn to modify domain classes and views. In addition you learn details of form input and image displaying. As a bonus, you will also learn about Geolocation and Google Maps APIs.
You will continue working on the book store website. The book store client asks that images of the book cover should appear in the list of books on the book index page. The bookstore client also wants that his administer for the site should be able to upload images of the cover. Your client’s request implies that the Book domain class should have a “cover” property that either stores the image file or the URI (file path) to the image file. In this tutorial, we’ll store the image file in the database. Grails makes store the image file in the database easier then storing the URI of the file in the database.
Considering the client demands, your modifications to the website needs to modify:
- The Book domain by adding a “cover” property
- The views/book/create.gsp, _form.gsp and edit.gsp to upload the cover image file
- The Book controller by adding an action to stream the image in the database to the views
- The views/book/show by adding the cover image to the file
- Finally the Book controller and the views/book/index.gsp to add the cover image to the list
Step 1: Modify Book Domain
We about file upload in the Grails documentation
http://grails.github.io/grails-doc/latest/guide/theWebLayer.html#uploadingFiles
Study in particular the section “File Uploads through Data Binding.”
You should add the “cover” property to the Book domain:
byte[] cover
We also need to add to the constraints mapping
static constraints = { title() cover(nullable: true, maxSize: 1024*1024*2 ) }
The cover maxSize must be specified or the database will not be created with an entry large enough to hold an image file. Also note that 2 megabytes might not be larger enough for photos in your citizen science project.
Step 2: Modify create, edit, and _froms views
To upload the cover image file we need to:
- Modify the <form> tag by specifying the encoding of the submitted data to “mulitpart/form-data”
- Add <input type=”file> to the form so that user can browser and select the file to upload.
Grails scaffolding makes the template _form.gsp for the body of the form, while the <form> tags are in the create and edit vie
i. Modify create and edit views
Study the create and edits views. You should locate the form tag
<g:form url="[resource:bookInstance, action:'save']" >
Read about the form tag at w3schools
http://www.w3schools.com/html/html_forms.asp
Grails scaffolding uses a g-tag to create the html form tag. Read about the form g-tag in the reference documentation:
http://www.grails.org/doc/latest/ref/Tags/form.html
The g-form tag made by the scaffolding uses the url attribute, which is a map that can specify the controller, action, id and other information. The default controller is the controller for the view, in this case BookController. The “action” attribute specifies the controller action that will be invoked when the form is submitted. Modify the g-form tag to specify the encoding of the submitted data. Forms that upload files need to be “multipart/form-data.” Add the attribute to the form tag
enctype="multipart/form-data"
so the form tag looks like
<g:form url="[resource:bookInstance, action:'save']" enctype="multipart/form-data">
IMPORTANT NOTE: If the g-form tag in the update tag specifies the method of submission as “put”, meaning it contains the attribute method=”PUT”, delete the attribute. Although update actions should technically use the put method for submission, it does not work in many browsers. The post method always works and is the default method for form data that is “mulitpart”. So deleting method=”PUT” will make the submission method post.
On the side, you should notice the g-actionSubmit tag in the form. This is a g-tag for making the submit button. The g-actionSubmit tag can have an addition attribute “action” than the submit-tag. The action specified will override the action specified in the form tag. This enables a form data to have multiple submit buttons with different actions.
Notice the g-render tag in the form
<g:render template="form"/>
This tag tells Grails to insert the _form.gsp file. Grails precedes template file name with an underscore. So we should look for the body of the form in _form.gsp file.
IMPORTANT NOTE: Be sure to add
enctype="multipart/form-data"
to the g-from tag in both create and edit views. Also be sure to delete any occurrence of method=”PUT” or change it to method=”POST”.
ii. Modify _from.gsp
We need to add the file upload input to the form. Study views/book/_form.gsp. Notice that it is divided into multiple <div> tags, for example:
<div class="fieldcontain ${hasErrors(bean: bookInstance, field: 'author', 'error')} required"> <label for="author"> <g:message code="book.author.label" default="Author" /> <span class="required-indicator">*</span> </label> <g:select id="author" name="author.id" from="${cs4760progassign.Author.list()}" optionKey="id" required="" value="${bookInstance?.author?.id}" class="many-to-one"/> </div>
This div will display any validation errors. The hasError function parameter is a map. The “field” entry specifies property of the book domain. In the above example this is “author.” Inside the div is the label tag. The “for” attribute specifies the element id of the input tag associated with the label. In the example above this is “author”. Notice below the label-tag is the input tag. In this example for the author property it is a select from a list of all the authors in the database. The g-select tag also has an id attribute,
id="author"
It is the matching id for the “for” attribute in the label-tag.
Educate yourself on all the different form inputs by reading about input types at W3schools:
http://www.w3schools.com/html/html_form_input_types.asp
http://www.w3schools.com/html/html_form_attributes.asp
We need to add div for the file input type. At the bottom of the page copy and paste the code below
<div class="fieldcontain ${hasErrors(bean: bookInstance, field: 'cover', 'error')} "> <label for="cover"> <g:message code="book.cover.label" default="Cover" /> </label> <input style="display:inline" type="file" name="cover" id="cover" accept="image/*" capture /> </div>
Notice hasErrors field is now specified as “cover”. Also label-tag specifies that it is for the “cover” element which matches input-tap id attribute, “cover.” The g-message tag inside the label-tag will look for the “book.cover.label” in the i18n/messages.properties file. It will not find it in the file, so the default attribute is important. The default attribute, “Cover”, is what will be displayed for label.
The input-tag is below the label-tag. Its type is “file”, so when the user clicks the “file upload” or “browse” button a file picker window will appear. Notice that the input tag also has the accept attribute specified
accept="image/*"
This will filter the file picker to show only image files. For an explanation see W3schools
http://www.w3schools.com/tags/att_input_accept.asp
Notice that the input-tag also has the capture attribute. Less enables media capture device. For image capture this is the camera. The user can choose between uploading a file or making a photo with the camera. See the api standard
http://www.w3.org/TR/html-media-capture/
You can test the attribute on your phone at this site
http://mobilehtml5.org/ts/?id=23
The input-tag also has a styling attribute.
style="display:inline"
This is called inline styling, not because the of “inline” in “display:inline”, but because the styling is specified within the element tag using the style attribute. The value, “display:inline” forces the file upload box and button to appear in line with the label. Without this styling, the input tag is styled with display:block which will cause the file upload box and button to appear below the label.
Step 3: Modify BookController
i. Modify allowedMethods
Just below the “class BookController {“, you should find the “allowedMethods” map specification. Change the update entry to “POST”. It should look like
static allowedMethods = [save: "POST", update: "POST", delete: "DELETE"]
We need to do this because form in update is using the post method. I do not know why the put method is not allowed.
ii. Check File Upload to the Database
You will need some small image files. PNG files will do fine, but so will jpg files.
Stop and start the web app. You need to do restart the web app because the domain has been modified. After loading the url into the browser, log in as “admin,” so that you can upload the file. Go to
cs4760programassign/book/list
Click the “new book” button and fill out the form. Note that you can create a new book with or without a cover image file. Make at least one new book with a file for the cover. After uploading a file, go to dbconsole to view the database. In dbconsole, click on the book table in the left panel,
"SELECT * FROM BOOK
should appear in the SQL statement window. Click the Run button. You should see that “COVER” entry and has data for the book that you uploaded an cover image file.
iii. Add showCover method.
To display the cover image, we need to stream the database entry into the response. Read about the “response” object in the reference manual:
http://www.grails.org/doc/latest/ref/Servlet%20API/response.html
The example demonstrates the use of response.outputStream with the bit shift operator, “<<“.
We will locate the image in the web page using the img-tag. We about the img-tag at w3School
http://www.w3schools.com/tags/tag_img.asp
The img-tag includes a “src” attribute, which generally specifies a URI for the image file. Instead of specifying a file path URI on the server, we will specify a URI pointing to a controller action.
In BookController add the showCover method by copy and pasting the code below
@Secured(['permitAll']) def showCover(){ if(debugCover){ println " " println "In showCover" println "params.id: "+params.id } def coverInstance = Book.get(params.id) response.outputStream << coverInstance.cover response.outputStream.flush() }
The line that gets the coverInstance uses “params” to get the book id from the URL. Read about the params object in the Grails reference manual:
http://www.grails.org/doc/latest/ref/Controllers/params.html
Step 4: Modify views/book/show
We’ll show the image in the detail view of the book, views/book/show.gsp. In the show.gsp you notice in the center of the file an order list of book properties.
<ol class="property-list book"> … </ol>
The first list item is
<g:if test="${bookInstance?.author}"> <li class="fieldcontain"> <span id="author-label" class="property-label"> <g:message code="book.author.label" default="Author" /> </span> <span class="property-value" aria-labelledby="author-label"> <g:link controller="author" action="show" id="${bookInstance?.author?.id} ${bookInstance?.author?.encodeAsHTML()} </g:link> </span> </li> </g:if>
A g-if tag surrounds the list item. Read about the g-if tag in the Grails reference manual
http://www.grails.org/doc/latest/ref/Tags/if.html
The g-if tag check that bookInstance has the author property. After the g-if tag, the g-message tag display the label for the property. In this case, the default, “Author”, is used. After the message tag the g-link specifies the controller action to get the author property. Read about the g-link tag
http://www.grails.org/doc/latest/ref/Tags/link.html
You can read about encoding at
http://www.grails.org/doc/latest/guide/security.html
At the bottom of the list add the code for displaying the cover image by copying and pasting the code below just above the end tag for the order list, </ol>.
<g:if test="${bookInstance?.cover}"> <li class="fieldcontain"> <span id="cover-label" class="property-label"> <g:message code="book.cover.label" default="Cover" /> </span> <img alt="Missing Cover" src="${createLink(controller:'book', action:'showCover', id:"${bookInstance.id}")}"> </li> </g:if>
The code uses the g-if tag to check for the bookInstance.cover. The message tag use the default value, “Cover”, to display the label for the cover property. Note that the scr attribute in the img-tag use the createLink function to make a URI pointing to BookController.showCover action. It also specifies the id portion of the URI.
You should test that the image displays in the show view. You should not need to stop and start the web app because we have only modified a view. Point the browser to
cs4760progassign/book/list
and click on a list item to show the details of a book that you have uploaded a cover image for. It should display.
Step 5: Add Map to book/home
This step is a diversion and using the Geolocation API and Google Maps API. Our goal is detect the user’s current location and display it on a map in the home page.
i. Use geolocation
Read about using the geolocation object at
https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/Using_geolocation
You can learn more about the Gelocation API at
http://www.w3.org/TR/geolocation-API/
On the home/index.gsp, we need to add a div for the latitude and longitude to display. In addition, we’ll add a div for where to display the map. At the bottom of home/index.gsp add the code below just below the terminating body-tag.
<!-- Simple get location --> <p style="margin-top:20px"><button onclick="geoFindMe()" >Show my location</button></p> <div id="latlong-out"></div> <div id="map-canvas" style="width:500px; height:400px"></div>
The button will call the geoFindMe function. We will add this function in a JavaScript file. We’ll display the latitude and longitude in the div with id equal to “latlong-out” and the map will display in the div with id equal to “map-canvas”. Note that the map-canvas must have a width and height, or other wise it will not show.
We will write the geoFindMe function in a JavaScript file called geoloc.js. We will need to include the JavaScript file, geoloc.js, to the home/index.gsp in order to access the navigator. Because the JavaScript in geoloc.js will only be used on this page of the website, we should load it only on this page. We will use the asset-pipeline to include the file. Between <head> and </head>, copy the line below
<asset:javascript src="geoloc.js"/>
Now we need to make the geoloc.js file. In grails-app/assets/javascripts/ add a file and name it geoloc.js. When the editor displays the blank page copy the code below into the file
function geoFindMe() { var outputLatLong = document.getElementById("latlong-out"); var outputMap = document.getElementById("map-canvas"); if (!navigator.geolocation){ outputLatLong.innerHTML = "<p>Geolocation is not supported by your browser</p>"; return; } function success(position) { var latitude = position.coords.latitude; var longitude = position.coords.longitude; outputLatLong.innerHTML = '<p>Latitude is ' + latitude + '° <br>Longitude is ' + longitude + '°</p>'; // map image var img = new Image(); img.src = "http://maps.googleapis.com/maps/api/staticmap?center=" + latitude + "," + longitude + "&zoom=13&size=300x300&sensor=false"; outputMap.appendChild(img); // end of map image }; function error() { outputLatLong.innerHTML = "Unable to retrieve your location"; }; outputLatLong.innerHTML = "<p>Locating…</p>"; navigator.geolocation.getCurrentPosition(success, error); }
The geoloc.js defines only the geoFindMe function. The function first locates the html elements for writing the latitude and longitude and displaying the map. The are given name in the script outputLatLong and outputMap. Then the script checks if it can access the navigator.geolocation. After that, it defines the callback for success and error. At the bottom of the file the script first writes “Locating …” in the div to show the latitude and longitude.
Review the success callback. It first assigns latitude and longitude from the position.coords. Then it adds it to the html. Finally, it makes an Image and assign the source to the a map at maps.googleapis.com. Note that this is a static map. Also note this example of a get and the latitude and longitude is added to the URL.
Save your all the files and test. The browser built into ggts will not display the coordinates. This is just like the ajax call you made in the previous assignment. Use an external browser to test your code. Point the browser to /cs4760programassign/home. Click the button. A modal should appear telling you that the website is accessing your location and if you want to allow or block. Be sure to allow or the geolocation will not be accessed. Both the latitude and longitude coordinates and the map should appear.
ii. Use Google Map API
The map is good but it is static and it is hard to add markers to it. We need to use Google Maps API to manipulate the map and add markers.
Read about adding a Google Map at
https://developers.google.com/maps/tutorials/fundamentals/adding-a-google-map
You can learn more about Google Map API at
https://developers.google.com/maps/documentation/javascript/tutorial
First we need to include the the Google Map API javascript on the home/index.js. In home/index.gps and the line below to the bottom of the view just above the terminating body-tag, </body>
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js"></script>
Note that Google no longer requires using a key.
Now we need to modify the script in geoloc.js to use the google maps api. In geoloc.js file, comment out the lines between “// map image” and “// end of map image” in the success function. Then add the code below to the success function
var coordinates = new google.maps.LatLng(latitude, longitude); var mapOptions = { center: coordinates, zoom: 8 }; var map = new google.maps.Map(outputMap, mapOptions); // Set up and display the Marker var markerOptions = { map: map, position: coordinates, draggable: false, animation: google.maps.Animation.DROP }; var maker = new google.maps.Marker(markerOptions);
Notice how the google.maps.Map object is made. The options for the map are set in the mapOptions object. Then the Map is created with reference to the html element to display the map, outputMap, and the map options, mapOptions. The marker on the map is set up similar to the map. The marker is configured by the markerOptions object. Notice that the marker is associated with a map in the morker options by the map property. The position of the marker is set by the position property. We use the coordinates from getCurrentLocation. You can guess what the draggable and and animation property mean.
Save the files and refresh the /home/index page. Notice that you can drag the map but not marker. Try changing the draggalble markerOptions property and confirm your guess what draggable means.
Step 6: Modify book/index.gsp and BookController.index()
You are on your own for this step. The goal is to display the book cover in the list of books on the book index page. Show the cover above or below each list item for the book in the list.
Some hints:
- You’ll probably want to add the book.id to the bkAuthor map in the BookController.index action.
- Try styling the img-tag using twitter bootstrap. Try the img-thumbnail class.
http://www.w3schools.com/bootstrap/bootstrap_ref_css_images.asp
When you are done, email me a screen shoot of the book index page. Use the subject line
cs4760 Programming Assignment Proof 4