Objectives

Implement a phase 1 of solution to assignment 1

Spacebook Baseline

Even if you have completed a substantial amount of the assignment, download and include this projects here into your workspace:

(You must run 'play eclipsify' before importing)

Run the app and familiarize yourself with the features as implemented so far.

Moving posts to their own Views

Introduce a a new 'component' view:

Views/components/displaypostsummaries.html

<section class="ui stacked segment">
  <h4 class="ui inverted blue block header">Published Posts</h4>
  #{list items:user.posts, as:'post'}
    <div class="ui item"> 
      <i class="url large basic icon"></i>
      <a href="/blogpost/view/${post.id}"> ${post.title} </a> 
    </div>
  #{/list}
</sectiion>

Change Views/Blog/index.html view to include the 'displaypostsummaries.html instead of 'displayposts.html':

View/Blog/index.html

...
<h1 class="ui inverted teal block header" >Blog  </h1>
<section class="ui two column grid segment">
  <div class="row">
    <div class="ui column">
      #{include "components/createpost.html" /} 
    </div>
    <div class="column">
      #{include "components/displaypostsummaries.html" /} 
    </div>
  </div>
</section>

Introduce a new controller called 'BlogPost'.

controllers/BlogPost:

package controllers;

import java.util.List;

import models.Message;
import models.Post;
import models.User;
import play.Logger;
import play.mvc.Controller;

public class BlogPost  extends Controller
{
  public static void view(Long postid)
  {
    Logger.info("Post ID = " + postid);
    Post post = Post.findById(postid);
    render (post);
  }
}

With a matching view:

Views/BlogPost/view.html

#{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>

This needs a matching route:

# BlogPost
GET   /blogpost/view/{postid}                   BlogPost.view

This should establish each post with its own view, and links to these views from the main blog view. Try it out now and make sure it works before proceeding.

Delete Post Support

To delete a post, we need this method in the Blog Controller:

controllers/Blog

  public static void deletePost(Long postid)
  {    
    User user = Accounts.getLoggedInUser(); 
    Post post = Post.findById(postid);
    Logger.info ("Request to delete title:" + post.title + " content:" + post.content);

    user.posts.remove(post);

    user.save();
    post.delete();

    index();
  }

This route:

conf/routes:

GET   /blog/deletepost/{postid}                 Blog.deletePost

We then insert a button onto the end of each post:

Views/BlogPost/view.html

...
<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>

Run this now, and verify that you can safely delete posts.

Comment model + Tests

Introduce a new Commment model class:

models/Commet:

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();
  }
}

This models the comment text, the author + the time when the comment was created.

Extend the Post class to use keep a collection of these:

models/Post:

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

We need a test to make sure this actually works:

test/CommentTest:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import models.Comment;
import models.Message;
import models.Post;
import models.User;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import play.test.Fixtures;
import play.test.UnitTest;


public class CommentTest  extends UnitTest
{
  private User bob;
  private Post aPost;

  @BeforeClass
  public static void loadDB()
  {
    Fixtures.deleteAllModels();
  }

  @Before
  public void setup()
  {
    bob   = new User("bob", "jones", "bob@jones.com", "secret",20, "irish");
    aPost = new Post ("Post A", "This is the post A content");
    bob.posts.add(aPost);
    aPost.save();
    bob.save();
  }

  @After
  public void teardown()
  {
    bob.delete();
    aPost.delete();
  }

  @Test
  public void testAddDeleteComment()
  {
    User user = User.findByEmail("bob@jones.com");
    Comment comment1 = new Comment(user, "Comment 1");
    comment1.save();


    Post post = user.posts.get(0);
    post.comments.add(comment1);
    post.save();
    user.save();

    User anotherUser = User.findByEmail("bob@jones.com");
    assertEquals("Comment 1", anotherUser.posts.get(0).comments.get(0).content);

    post.comments.clear();
    post.save();
    comment1.delete();

    User yetAnotherUser = User.findByEmail("bob@jones.com");
    assertEquals(0, yetAnotherUser.posts.get(0).comments.size());
  }
}

Run the application in test mode:

play test

and then browse to:

and see if all the tests run correctly.

Comment UI

This time we will start with a new route:

conf/routes:

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

We need to extend the BlogPost controller to respond to this route:

  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);
    comment.save();

    post.comments.add(comment);
    post.save();
    view(postid);
  } 

(Import comment to get this to compile correctly)

Replace the BlogPost view with the following:

views/BlogPost/view.html

#{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>

Read the above very carefully - and note how the comments are listed after the post - and that a comment form is included at the end to allow new comments to be created. Also, make sure you can delete posts without incident.

We will make one more subtle change to make sure our delete post cleans up any attached comments correctly. Change the Post model as follows:

models/Post:

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

The 'cascade' means that when we delete a post, all comments will be deleted as well.

Exercises

Even if you have implemented the steps correctly, down, unzip and import this solution:

spacebook-assignment-part-1.zip

Compare this solution so far with your own assignment.

  • Are there differences in your approach?