Objectives

Story 1

The starting point for this lab is the final version of lab 5:

Story

This assignment must be submitted as a url for a cloudbees hosted version of the project. If you are unsure about deployment, have mislaid your keys or generally failed to deploy to date, then this must be tackled immediately.

The labs from Web Development are still online - and you can go though those if necessary. You can also seek help in the labs on Fridays.

Story 2

Problem

In the current version - and 'edit profile’ button appears on the profile page. When opened, this page has no navigation bars, just a 'save' button, which takes you back to the profile view.

Change this such that a new menuitem appears on the menu bar - perhaps on the extreme right. It should perhaps be renamed 'account settings'. This menu will enable the account setting to be edited as currently implemented. However, the menu bar should remain on screen.

Solution 1

This is a revised menu structure:

<nav class="ui inverted main menu">
  <div class="title item"> Spacebook </div>
  <a class="item" href="/home">Home</a>
  <a class="item" href="/members">Members</a>
  <a class="item" href="/profile">Profile</a>
  <a class="active item" href="/blog">Blog</a>
  <div class="right menu">
    <div class="ui dropdown item">
      <i class="user icon"></i>
      <div class="menu">
        <a class="item" href="/editprofile">Settings</a>  
        <a class="item" href="/logout">Logout</a> 
      </div>
    </div>
  </div>
</nav> 

Try it out on just one of the views - say the blog view .

You should notice that it does not work. This is because it is useing a 'dropdown' control, which needs to be specifically enabled.

Do this by changing views/main.html - changing the body as follows:

  <body>
    #{doLayout /}  
    <script>
      $('.ui.dropdown').dropdown();
    </script>
  </body>

In the above, we have an extra <script> tag, which primes the dropdown control.

Test again now, and it should work.

Solution 2

We have only got our new option working on a single view. We could start the process of copying / pasting this into every view, which is rather tedious.

It would be better if we can have the menu in a single place. In views, create a new folder called tags. Inside this folder, create a new file called mainmenu.html. Paste in the following contents:

<nav class="ui inverted main menu">
  <div class="title item"> Spacebook </div>
  <a id="home"    class="item" href="/home">Home</a>
  <a id="members" class="item" href="/members">Members</a>
  <a id="profile" class="item" href="/profile">Profile</a>
  <a id="blog"    class="item" href="/blog">Blog</a>
  <div class="right menu">
    <div class="ui dropdown item">
      <i class="user icon"></i>
      <div class="menu">
        <a class="item" href="/editprofile">Settings</a>  
        <a class="item" href="/logout">Logout</a> 
      </div>
    </div>  
   </div> 
</nav> 

<script>
  $("#${_id}").addClass("active item");
</script>

Now, back in your blog view, completely replace the existing menu with the following single line:

#{mainmenu id:"blog"/}

Test this - and verify it works as expected.

Now you can replace the menu on all of the views with new 'tag' we are using above, with a different id for each as appropriate.

#{mainmenu id:"home"/}
...
#{mainmenu id:"members"/}
...
#{mainmenu id:"profile"/}
...
#{mainmenu id:"blog"/}

This solution uses custom tags feature, which is the ability to write our own tags. We will discuss this further in class.

Story 3: New Loook and Feel

Problem

Redesign the UI with different choices for:

For inspiration, download the semantic ui archive again, and explore the ‘examples’ directory.

Solution

There are yet to be developed a range of good examples for SemanticUI. See discussion here:

As soon as they are ready, they might start to appear here:

In the meantime, this looks like the most ambitious use of the framework:

You might be able to inspect the source there and get some ideas / see what is possible.

Story 4: Revised Home Page

Problem

The Home page is to be revised as follows:

Solution

The simplest solution might be to include two of the 'components' directly into the home view, as another row:

<h1 class="ui inverted teal block header" >${user.firstName} ${user.lastName}</h1>
<section class="ui two column grid segment">
  <div class="row">
    <div class="ui column">
      #{include "components/friends.html" /} 
    </div>
    <div class="column">
      #{include "components/messages.html" /} 
    </div>
  </div>
  <div class="row">
    <div class="column">
      #{include "components/statusupdate.html" /} 
    </div>   
    <div class="column">
      #{include "components/profileimages.html" /} 
    </div> 
  </div>
</section>

This suffers from two drawbacks:

Issue 1:

Duplicate the changeStatus action in the Home controller:

  public static void changeStatus(String statusText)
  {
    User user = Accounts.getLoggedInUser();
    user.statusText = statusText;
    user.save();
    Logger.info("Status changed to " + statusText);
    index();
  }

Create a new route to direct to this action:

POST    /home/changestatus                      Homee.changeStatus

Create a new file called statusupdate.html in the views/tags folder, and paste in the following:

<section class="ui stacked segment">
  <h4 class="ui inverted red block header"> Status</h4>
  <div class="ui message">
    <p> ${_status} </p>
  </div>
   <section class="ui form segment">
    <form action="${_route}" method="POST">
      <div class="field">
        <textarea name="statusText"></textarea>
      </div>
      <button class="ui button teal submit labeled icon"><i class="icon edit"></i> Update </button>
    </form>
  </section>
</section>

Back in the views/home.html, replace this include:

    <div class="column">
      #{include "components/statusupdate.html" /} 
    </div>   

with this:

    <div class="column">
      #{statusupdate route:"/home/changestatus", status:"${user.statusText}" /} 
    </div> 

This is quite sophisticated, but represents best practice in building server side template.

Issue 2:

We can take the same approach. First, duplicate this method in Home:

  public static void uploadPicture(Long id, Blob picture)
  {
    User user = User.findById(id);
    user.profilePicture = picture;
    user.save();
    Logger.info("saving picture");
    index();
  } 

This new route:

POST    /home/uploadpicture/{id}                Home.uploadPicture

Create a new file called profileimage.html in the views/tags folder:

<section>
  <div class="ui medium image">
    <label class="ui ribbon label">Profile Image</label>
    <img src="${_image}">
  </div>  
  <section class="ui raised form segment">
    <form action="${_route}" method="post" enctype="multipart/form-data">     
      <div class="ui field">
        <input type="file" name="picture">  </input> 
     </div>     
     <button class="ui blue submit button"> Upload</button>       
    </form>        
  </section>
</section>

We should now include this in the home/index.html file:

    <div class="column">
      #{profileimage route:"/home/uploadpicture/${user.id}", image:"/profile/getpicture/${user.id}" /} 
    </div> 

See if you can understand the flow of information in the above, particularly the paramaters passed to profileimage.html from home/index.html.

Further Exercise

See if you can now refactor the profile/index.html to follow the above pattern.

Story 5: User Logged in Status

Problem

The home page currently displays a list of message from other users in a table.

Solution

Introduce a new field into user:

  public boolean online;

In Accounts controller - set to true when user logs in:

    if ((user != null) && (user.checkPassword(password) == true))
    {
      Logger.info("Authentication successful");
      session.put("logged_in_userid", user.id);
      user.online = true;
      user.save();
      Home.index();
    }

... and false when he/she logs out:

  public static void logout()
  {
    String userId = session.get("logged_in_userid");
    User user = User.findById(Long.parseLong(userId));
    user.online = false;
    user.save();
    session.clear();
    index();
  }

This is a new version of the members page which makes use of the online member of user:

<section class="ui stacked segment">
  <div class="ui list"> 
    #{list items:users, as:'user'}
      <div class="ui item"> 
        #{if user.online}
          <i class="user green icon"></i>
        #{/if}
        #{else}
          <i class="user red icon"></i>
        #{/else}
        ${user.firstName} ${user.lastName}: : ${user.statusText}  <a href="members/follow/${user.id}"> (follow) </a> 
      </div>
    #{/list}
  </div>
</section>

Here is a new version of friends.html to keep the home page updated with user status:

<section class="ui stacked segment">
  <h4 class="ui inverted red block header">Friends (${user.friendships.size()})</h4>
  <div class="ui list"> 
    #{list items:user.friendships, as:'friendship'}
      <div class="ui item"> 
        #{if friendship.targetUser.online}
          <i class="user green icon"></i>
        #{/if}
        #{else}
          <i class="user red icon"></i>
        #{/else}
        <a href="/publicprofile/${friendship.targetUser.id}"> ${friendship.targetUser.firstName} ${friendship.targetUser.lastName} </a>
         (<a href="/home/drop/${friendship.targetUser.id}"> drop </a>)
      </div>
    #{/list}
  </div> 
</section> 

Note that the above solution is a major flaw. If the user does not log out, and just exits the browser, then he/she will still be registered as 'logged in'.

We really need a solution that has some form of timeout, setting the user to logged out if a certain amount of time without activity has elapsed.

Story 6: Basic Blog

Problem

Each user is now to also have a blog. This will appear on a separate tab.

A blog it to provide a facility to enable the user to create a post.

A post is just any text the user may enter.

This post is to be listed in reverse chronological order on the blob page (newest first)

Solution

Already implemented in the base version of this lab.

Story 7: Basic blog with Comments

Problem

For each post, a button should enable a user to leave a comment.

The comments should appear under any given post, and should include the name and the user who left the comment + the date.

Solution

New model for a Comment:

package models;

import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;

import play.db.jpa.Model;

@Entity
public class Comment extends Model
{
  @Lob
  public String content;

  public Comment(String content)
  {
    this.content = content;
  }
}

New entry in Post model:

  @OneToMany (cascade=CascadeType.ALL)
  public List<Comment> comments = new ArrayList<Comment>();

New action in the BlogPost controller:

  public static void newComment(Long postid, String content)
  {    
    Logger.info("Post ID = " + postid);
    Post post = Post.findById(postid);
    Comment comment = new Comment(content);    
    post.comments.add(comment);
    post.save();
    view(postid);
  } 

with a matching route for this action:

POST  /blogpost/comment/{postid}                BlogPost.newComment

The blogpost view then is extended to support comments:

#{extends 'main.html' /}
#{set title:'Blog' /}

<nav class="ui inverted menu">
  <header class="header item"> Spacebook </header>
  <div class="right menu">
    <a class="active item" href="/blog">Blog</a>
  </div>
</nav> 

<section class="ui stacked segment">
  <h5 class="ui inverted green block header"> ${post.title}</h5>
  <p> 
    ${post.content}:
  </p>
  <a href="/blog/deletepost/${post.id}" class="ui button red labeled icon"><i class="icon delete"></i> Delete </a>

  <section class="ui stacked segment">
    <h5 class="ui inverted blue block header">Comments:</h5>
    #{list items:post.comments, as:'comment'}
      <p> 
        ${comment.content}
        <hr>
      </p>
    #{/list}  
  </section>

</section>

<section class="ui form segment">
  <h4 class="ui inverted blue block header">Add a comment</h4>
  <form action="/blogpost/comment/${post.id}" method="POST">
    <div class="field">
      <textarea name="content" placeholder="Content"></textarea>
    </div>
    <button class="ui button green submit labeled icon"><i class="icon edit"></i> Add Comment </button>
  </form>
</section>

The above version does not include date / time or author.

Author Support

First, tackle author. We will need to record the author in the model:

package models;

import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;

import play.db.jpa.Model;

@Entity
public class Comment extends Model
{
  @Lob
  public String content;

  @OneToOne 
  public User author;

  public Comment(User author, String content)
  {
    this.author  = author;
    this.content = content;
  }
}

Then, in newComment action, we need to pass this to the new user:

  public static void newComment(Long postid, String content)
  {    
    Logger.info("Post ID = " + postid);
    Post post = Post.findById(postid);

    User user = Accounts.getLoggedInUser();
    Comment comment = new Comment(user, content);    
    post.comments.add(comment);
    post.save();
    view(postid);
  }

The author is the currently logged in user.

Finally, we can display the author after the comment text in BlogPost/view.html:

        ${comment.content} : ${comment.author.firstName}

Time Date Support

This is a revised version of comment which will contain a date/time stamp:

package models;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;

import play.db.jpa.Model;

@Entity
public class Comment extends Model
{
  @Lob
  public String content;

  @OneToOne 
  public User author;

  public Date postedAt;

  public Comment(User author, String content)
  {
    this.author  = author;
    this.content = content;
    this.postedAt = new Date();
  }
}

With the time stamp being recorded, we can extend the view to print it for each comment:

        ${comment.content} : ${comment.author.firstName} : on : ${comment.postedAt.format('dd MMM yy HH:mm:ss')}

Story 8: Public Blog

Problem

The url of the blog should be 'public'. I.e any user (whether logged in or not) can read the blog if they have the correct url. Only logged in users can leave comments however.

Solution

Make a new route as follows:

GET   /blog/publicblog/{userid}                 Blog.publicBlog

Introduce this new action into Blog.java controller:

  public static void publicBlog(Long userid)
  {
    User user = User.findById(userid);
    render(user);
  }

And this matching view in views/Blog/publicblog.html

#{extends 'main.html' /}
#{set title:'Blog' /}

<nav class="ui inverted menu">
  <div class="title item"> Spacebook </div>
</nav> 

<h1 class="ui inverted red block header" >${user.firstName} ${user.lastName} 's Blog</h1>

Try this now, and verify that these pages appear:

To display the blog, extend the view as below:

#{extends 'main.html' /}
#{set title:'Blog' /}

<nav class="ui inverted menu">
  <div class="title item"> Spacebook </div>
</nav> 

<h1 class="ui inverted red block header" >${user.firstName} ${user.lastName} 's Blog</h1>

<section class="ui stacked segment">
  <h2 class="ui inverted blue block header">Published Posts</h4>
  #{list items:user.posts, as:'post'}
    <h3 class="ui inverted green block header"> ${post.title}</h4>
    <p> ${post.content} </a> 
  #{/list}
</section>

This version does not show comments, but make sure it works before going further.

We can expand the main section to display comments:

<section class="ui stacked segment">
  <h2 class="ui inverted blue block header">Published Posts</h4>
  #{list items:user.posts, as:'post'}
    <h3 class="ui inverted green block header"> ${post.title}</h4>
    <p> ${post.content} </a> 

    <section class="ui raised segment">
    <h5 class="ui inverted teal block header">Comments:</h5>
    #{list items:post.comments, as:'comment'}
      <p> 
        ${comment.content} : ${comment.author.firstName} : on : ${comment.postedAt.format('dd MMM yy HH:mm:ss')}
        <hr>
      </p>
    #{/list}  
    </section>

  #{/list}
</section>

Try this now on a blog with comments.

If we want to leave comments, we should only permit it for logger in users. Here is one approach. Rework the action as follows:

  public static void publicBlog(Long userid)
  {
    String userId     = session.get("logged_in_userid");
    User loggedInUser = User.findById(Long.parseLong(userId));

    User user = User.findById(userid);
    render(user, loggedInUser);
  }

Look at it carefully - we are passing two parameters to the view, the user whose blog we want to display + the currently logged in user. The logged in user may be null if there is now one logged in.

Whether a user is logged in or not should be visible to the user. Here is an alternative menu for views/Blog/publicblog.html

<nav class="ui inverted menu">
  <a  class="item" href="/home">Spacebook</a>
  <div class="right menu">
    #{if loggedInUser}
      <div class="item"> Logged in as ${loggedInUser.firstName} ${loggedInUser.lastName} </div>
    #{/if}
    #{else}
      <a class="item" href="/login"> Log in to comment </a>
    #{/else}
  </div>
</nav> 

Try this now. If you are browsing a blog while not logged in, it should be apparent in the UI. If you are logged in, then the account name should appear in the top right.

We can now rework the entire view to display a comment form if the user is logged in:

#{extends 'main.html' /}
#{set title:'Blog' /}

<nav class="ui inverted menu">
  <a  class="item" href="/home">Spacebook</a>
  <div class="right menu">
    #{if loggedInUser}
      <div class="item"> Logged in as ${loggedInUser.firstName} ${loggedInUser.lastName} </div>
    #{/if}
    #{else}
      <a class="item" href="/login"> Log in to comment </a>
    #{/else}
  </div>
</nav> 

<h1 class="ui inverted red block header" >${user.firstName} ${user.lastName} 's Blog</h1>

<section class="ui stacked segment">
  <h2 class="ui inverted blue block header">Published Posts</h4>
  #{list items:user.posts, as:'post'}
    <h3 class="ui inverted green block header"> ${post.title}</h4>
    <p> ${post.content} </a> 

    <section class="ui raised segment">
    <h5 class="ui inverted teal block header">Comments:</h5>
    #{list items:post.comments, as:'comment'}
      <p> 
        ${comment.content} : ${comment.author.firstName} : on : ${comment.postedAt.format('dd MMM yy HH:mm:ss')}
        <hr>
      </p>                 
    #{/list}  
    #{if loggedInUser}
      <section class="ui form segment">
        <h4 class="ui inverted blue block header">Add a comment</h4>
        <form action="/blogpost/comment/${post.id}" method="POST">
          <div class="field">
            <textarea name="content" placeholder="Content"></textarea>
          </div>
          <button class="ui button green submit labeled icon"><i class="icon edit"></i> Add Comment </button>
        </form>
      </section>
    #{/if}
    </section>   
  #{/list}
</section>

Note the last section - we only display the form for filling in a comment if the user is logged in.

Try this now, and try to leave a comment. Look very carefully at the blog you actually leave a comment on. Do you notice anything odd? The problem with the above solution is that we are leaving comments on the wrong blog!

We need a new action for adding comments, bring this into the Blog controller:

  public static void newComment(Long userid, Long postid, String content)
  {    
    Logger.info("Post ID = " + postid);
    Post post = Post.findById(postid);

    User user = Accounts.getLoggedInUser();
    Comment comment = new Comment(user, content);    
    post.comments.add(comment);
    post.save();
    publicBlog(userid);
  }  

The supporting route is the most complicated we have seen:

POST  /blog/publicblog/{userid}/newcomment/{postid}  Blog.newComment

Try this now and see the comments will actually resolve as expected to the correct blog/post.

Story 9: Members Blogs

Problem

On the members page, provide a direct link to the blogs of each member

Solution

We have our members currently listed in the components/memnbers.html. Here is a new version to display the blog links:

  <table class="ui table segment">
    <tbody>
      <tr>
        #{list items:users, as:'user'}
        <tr>
          <td>
            #{if user.online}
              <i class="user green icon"></i>
            #{/if}
            #{else}
              <i class="user red icon"></i>
            #{/else}
          </td>
          <td>
            ${user.firstName} ${user.lastName}
          </td>
          <td>
            ${user.statusText}  
          </td>
          <td>
            <a href="members/follow/${user.id}"> (follow) </a> 
          </td>  
          <td>
            <a href="/blog/publicblog/${user.id}"> Blog </a>
          </td>
        </tr>
        #{/list}
    </tbody>
  </table>

We have placed all the fields in a table.

Story 10: Blog List

Problem

On the start page - displayed before a user logs in - display a set of links to the the users who have blogs. If a user has not created any posts, do not show any link.

Solution

In the Accounts/index.html we can bring in the members component we just created:

#{extends 'main.html' /}
#{set title:'Welcome to Spacebook' /}

<nav class="ui inverted menu">
  <header class="header item"> Spacebook </header>
  <div class="right menu">
    <a class="item" href="/signup"> Signup  </a>
    <a class="item" href="/login">  Login   </a>
  </div>
</nav> 

<section class="ui raised segment">
  <p> Signup or Log in to the Spacebook Service </p>

  #{include "components/members.html" /} 
</section>

Test this. Nothing is displayed! This is because the view does not have any users list to display.

Open the Accounts controller and change the index action as follows:

  public static void index()
  {
    List<User> users = User.findAll();
    render(users);
  }

Try again - and the list should appear on the start page.

Exercises

This is the solution at this stage:

This is a second version:

which has 2 key differences:

There were several excellent solutions from the class. Here are three which you might find useful to inspect:

Thanks to the guys for agreeing to have their work featured here