Staring up the project(Yabe)


Modification: 郭朝益(Kuo, ChaoYi), 2010-12-16 

Starting up the project (Scala version)-Memo

At this time, the content of Starting up the project (Scala version) is out of date.
This memo will try to update older descriptions on that page.

Prerequisites

Python 2.5(or later) is required in trying on GNU/Linux and Mac OSX platform.
As for Windows platform, playframework distriubtion has included python runtime, 
so you don't have to take care for python version. 

Installation of the play framework

Running the application

app/controllers/Application.scala
package controllers

import play._
import play.mvc._

object Application extends Controller {
def index = Template // Java == render( )
}

Starting with the User class

app/models/User.scala
package models
 
import java.util._
import play.db.jpa._

@Entity
class User(
var email: String,
  var password: String,
  var fullname: String,
var isAdmin: Boolean = false
) extends Model {
var isAdmin = false
override def toString = email
}
object User extends QueryOn[User]

Writing the first test

Before running 'play test',
 $ rm test/BasicTest.java

test/BasicTest.scala 
import org.junit._
import play.test._
import models._

import scala.collection.JavaConversions._

import org.scalatest.{FlatSpec,BeforeAndAfterEach}
import org.scalatest.matchers.ShouldMatchers
import scala.collection.mutable.Stack


class BasicTest extends UnitTest with FlatSpec with ShouldMatchers{
  it should "aVeryImportantThingToTest" in {
    2 should equal (1 + 1)
  }
}


test/BasicTest.scala


import org.junit._
import play.test._
import models._
import scala.collection.JavaConversions._
import org.scalatest.{FlatSpec,BeforeAndAfterEach}
import org.scalatest.matchers.ShouldMatchers
import scala.collection.mutable.Stack

class BasicTest extends UnitTest with FlatSpec with ShouldMatchers{
  it should "create and retrieve a user" in {
    // Create a new user and save it
    new User("bob@gmail.com", "secret", "Bob").save()

    // Retrieve the user with bob username
    var bob = User.find("byEmail", "bob@gmail.com") first

    // Test
    bob should not be (null)
    "Bob" should equal ( bob.fullname)
  }
}


app/models/User.scala
package models
import java.util._
import play.db.jpa._
@Entity
class User(
var email: String,
var password: String,
var fullname: String
) extends Model {
var isAdmin = false
}

object User extends QueryOn[User]{
def connect(email: String, password: String) = {
User.find("byEmailAndPassword", email, password).first
}
}

test/BasicTest.scala
it should "call connect on User" in {
// Create a new user and save it
new User("bob@gmail.com", "secret", "Bob").save()

// Test
User.connect("bob@gmail.com", "secret") should not be (null)
User.connect("bob@gmail.com", "badpassword") should be (null)
User.connect("tom@gmail.com", "secret") should be (null)
}


app/models/Post.scala
package models

import java.util._
import play.db.jpa._
import play.data.Validators._

@Entity
class Post(
@ManyToOne
var author: User,
var title: String,
@Lob
var content: String,
var postedAt: Date = new Date
) extends Model {
var postedAt = new Date()
}

object Post extends QueryOn[Post]


test/BasicTest.scala
...
class BasicTest extends UnitTest with FlatSpec with ShouldMatchers with BeforeAndAfterEach {
override def beforeEach() {
Fixtures.deleteAll()
}
...
}

test/BasicTest.scala
...
it should "create Post" in {
// Create a new user and save it
var bob = new User("bob@gmail.com", "secret", "Bob").save()

// Create a new post
new Post(bob, "My first post", "Hello world").save()

// Test that the post has been created
1 should equal ( Post.count())

// Retrieve all post created by bob
var bobPosts = Post.find("byAuthor", bob).fetch

// Tests
1 should equal ( bobPosts.size)
var firstPost = bobPosts(0)
firstPost should not be (null)
bob should equal (firstPost.author)
"My first post" should equal ( firstPost.title)
"Hello world" should equal ( firstPost.content)
firstPost.postedAt should not be (null)
}
...



Finish with Comment

app/models/Comment.scala
package models

import java.util._
import play.db.jpa._
import play.data.Validators._

@Entity
class Comment(
  @ManyToOne
  var post: Post,
  var author: String,
  @Lob
  var content: String,
var postedAt: Date = new Date
) extends Model{
  var postedAt = new Date()
}
object Comment extends QueryOn[Comment]

test/BasicTest.scala
...
it should "post Comments" in {
    // Create a new user and save it
var bob = new User("bob@gmail.com", "secret", "Bob").save()

// Create a new post
var bobPost = new Post(bob, "My first post", "Hello world").save()

// Post a first comment
new Comment(bobPost, "Jeff", "Nice post").save()
new Comment(bobPost, "Tom", "I knew that !").save()

// Retrieve all comments
var bobPostComments = Comment.find("byPost", bobPost).fetch()

// Tests
2 should equal (bobPostComments.size)

var firstComment = bobPostComments(0)
firstComment should not be (null)
"Jeff" should equal ( firstComment.author)
"Nice post" should equal ( firstComment.content)
firstComment.postedAt should not be (null)

var secondComment = bobPostComments(1)
secondComment should not be (null)
"Tom" should equal ( secondComment.author)
"I knew that !" should equal ( secondComment.content)
secondComment.postedAt should not be (null)
}
...

app/models/Post.scala
package models

import java.util.{Date,TreeSet,Set=>JSet,List=>JList,ArrayList}

import play.db.jpa._
import play.data.Validators._

@Entity
class Post(
@ManyToOne
@Required
var author: User,
var title: String,
@Lob @Required @MaxSize(10000) var content: String
) extends Model {
@Required
var postedAt = new Date()

@OneToMany(mappedBy="post", cascade=Array(CascadeType.ALL))
var comments: JList[Comment] = new ArrayList[Comment]

def addComment(author: String, content: String) = {
val newComment = new Comment(this, author, content)
newComment.save
comments.add(newComment)
this
}
}

test/BasicTest.scala
...
it should "use the comments relation" in {
// Create a new user and save it
var bob = new User("bob@gmail.com", "secret", "Bob").save()

// Create a new post
var bobPost = new Post(bob, "My first post", "Hello world").save()

// Post a first comment
bobPost.addComment("Jeff", "Nice post")
bobPost.addComment("Tom", "I knew that !")

// Count things
1 should equal (User.count())
1 should equal (Post.count())
2 should equal (Comment.count())

// Retrieve the bob post
bobPost = Post.find("byAuthor", bob).first
bobPost should not be (null)

// Navigate to comments
2 should equal (bobPost.comments.size)
"Jeff" should equal (bobPost.comments(0).author)

// Delete the post
bobPost.delete()

// Chech the all comments have been deleted
1 should equal (User.count())
0 should equal (Post.count())
0 should equal (Comment.count())
}
...

Using Fixtures to write more complicated test

data.yml includes date data like '2009-06-06', so you should confirm that
conf/application.conf inclues the following line,

date.format=yyyy-MM-dd

test/BasicTest.scala
...
it should "work if things combined together" in {
Fixtures.load("data.yml")

// Count things
2 should equal (User.count())
3 should equal (Post.count())
3 should equal (Comment.count())

// Try to connect as users
User.connect("bob@gmail.com", "secret") should not be (null)
User.connect("jeff@gmail.com", "secret") should not be (null)
User.connect("jeff@gmail.com", "badpassword") should be (null)
User.connect("tom@gmail.com", "secret") should be (null)

// Find all bob posts
var bobPosts = Post.find("author.email", "bob@gmail.com").fetch
2 should equal (bobPosts.size)

// Find all comments related to bob posts
var bobComments = Comment.find("post.author.email", "bob@gmail.com").fetch
3 should equal (bobComments.size)

// Find the most recent post
var frontPost = Post.find("order by postedAt desc").first

frontPost should not be (null)
"About the model layer" should equal (frontPost.title)

// Check that this post has two comments
2 should equal (frontPost.comments.size)

// Post a new comment
frontPost.addComment("Jim", "Hello guys")
3 should equal (frontPost.comments.size)
4 should equal (Comment.count())
}
...

Bootstrapping with default data

app/Bootstrap.scala
import play.jobs._
import play.test._
import models._
@OnApplicationStart
class Bootstrap extends Job{
  override def doJob {
    // Check if the database is empty
    if(User.count == 0) {
      Fixtures load "initial-data.yml"
    }
  }
}
controllers/Application.scala
package controllers
import play._
import play.mvc._
import models._
object Application extends Controller {
  def index = {
    val frontPost = Post.find("order by postedAt desc").first
    val olderPosts = Post.find("order by postedAt desc").from(1).fetch(10)
    render(frontPost, olderPosts)
  }
}
controllers/Application.scala
package controllers
import play._
import play.mvc._

import models._

trait Defaults extends Controller {
@Before
def setDefaults {
renderArgs += "blogTitle" -> configuration("blog.title")
renderArgs += "blogBaseline" -> configuration("blog.baseline")
}
}

object Application extends Controller with Defaults {
def index = {
val frontPost = Post.find("order by postedAt desc").first
val olderPosts = Post.find("order by postedAt desc").from(1).fetch(10)

render(frontPost, olderPosts)
}
}


Adding some pagination

models/Post.scala
...
def previous(): Post = {
    Post.find("postedAt < ? order by postedAt desc", postedAt).first
  }
  def next(): Post = {
    Post.find("postedAt > ? order by postedAt asc", postedAt).first
  }
...

Adding the comment form

controllers/Application.scala
  def postComment(postId: Long, author: String, content: String) {
val post = Post.findById(postId)
    post.addComment(author, content)
    show(postId)
  }

The Tag model object

models/Tag.scala
package models
import java.util._
import javax.persistence._
import play.db.jpa._
import play.data.validation._

@Entity
class Tag private (@Required var name:String) extends Model with Comparable[Tag] {
 override def toString() =  name
 override def compareTo(otherTag: Tag) = {
   name.compareTo(otherTag.name)
 }
}
object Tag extends QueryOn[Tag]{
  def findOrCreateByName(name: String):Tag = {
    Option(Tag.find("byName", name).first).
      getOrElse(new Tag(name).save())
  }
}
models/Post.scala
...

  @OneToMany(mappedBy="post", cascade=Array(CascadeType.ALL))
  var comments: JList[Comment] = new ArrayList[Comment]
  @ManyToMany(cascade=Array(CascadeType.PERSIST))
  var tags: JSet[Tag] = new TreeSet[Tag]
...

  def tagItWith(name: String):this.type = {
    tags add Tag.findOrCreateByName(name)
    this
  }
...

  object Post extends QueryOn[Post]{
    def findTaggedWith(tag: String) = {
      Post.find("select distinct p from Post p join p.tags as t where t.name = ?", tag).fetch
    }
}

test/BasicTest.scala
...
  it should "be able to handle Tags" in {
    // Create a new user and save it
    var bob = new User("bob@gmail.com", "secret", "Bob").save()

    // Create a new post
    var bobPost = new Post(bob, "My first post", "Hello world").save()
    var anotherBobPost = new Post(bob, "My second post post", "Hello world").save()

    // Well
    0 should equal (Post.findTaggedWith("Red").size)

    // Tag it now
    bobPost.tagItWith("Red").tagItWith("Blue").save()
    anotherBobPost.tagItWith("Red").tagItWith("Green").save()

    // Check
    2 should equal (Post.findTaggedWith("Red").size)
    1 should equal (Post.findTaggedWith("Blue").size)
    1 should equal (Post.findTaggedWith("Green").size)
  }

...

A little more difficult now

models/Post.scala 
  def findTaggedWith(tags: String*) = {
    Post.find("select distinct p.id from Post p join p.tags as t where t.name in (:tags) group by p.id having count(t.id) = :size", Map("tags" -> tags.toArray, "size" -> tags.size)).fetch
  }
test/BasicTest.scala
...
    1 should equal (Post.findTaggedWith("Red", "Blue").size)
    1 should equal (Post.findTaggedWith("Red", "Green").size)
    0 should equal (Post.findTaggedWith("Red", "Green", "Blue").size)
    0 should equal (Post.findTaggedWith("Green", "Blue").size)
...

The tag cloud

models/Tag.scala
...
object Tag extends QueryOn[Tag]{
  def findOrCreateByName(name: String):Tag = {
    Option(Tag.find("byName", name).first).
      getOrElse(new Tag(name).save())
  }

  def cloud = {
    Tag.find("select new map(t.name as tag, count(p.id) as pound) from Post p join p.tags\
 as t group by t.name").fetch
  }
}
test/BasicTest.scala
   ...
    var cloud = Tag.cloud
   "List({tag=Red, pound=2}, {tag=Blue, pound=1}, {tag=Green, pound=1})" should equal (cloud.toString())
   ...

Declaring the CRUD controllers

controllers/Admin.scala
package controllers

import play._
import play.mvc._
import play.data.validation._
import models._

object Comments extends Controller with CRUDFor[Comment]
object Posts extends Controller with CRUDFor[Post]
object Tags extends Controller with CRUDFor[Tag]
object Users extends Controller with CRUDFor[User]

Adding validation

models/User.scala
package models
import java.util._
import javax.persistence._
import play.db.jpa._
import play.data.validation._

@Entity
class User(
    @Email
    @Required    var email: String,
    @Required
    var password: String,
    var fullname: String
) extends Model {
  var isAdmin = false
  override def toString() = email
}
...
models/Post.scala
package models
import java.util.{Date,TreeSet,Set=>JSet,List=>JList,ArrayList}
import javax.persistence._
import play.db.jpa._
import play.data.validation._

@Entity
class Post(
  @Required
  @ManyToOne
  var author: User,
  @Required
  var title: String,
  @Required
  @Lob
  @MaxSize(10000)
  var content: String
) extends Model {
  @Required
  var postedAt = new Date()
...

models/Tag.scala
package models

import java.util._
import javax.persistence._
import play.db.jpa._
import play.data.validation._

@Entity
class Tag private (
  @Required var name:String
) extends Model with Comparable[Tag] {
 override def toString() =  name
...
models/Comment.scala
package models

import java.util._
import javax.persistence._
import play.db.jpa._
import play.data.validation._

@Entity
class Comment(
  @Required
  @ManyToOne
  var post: Post,
  @Required
  var author: String,
  @Required
  @Lob
  @MaxSize(10000)  var content: String
) extends Model{
  @Required
  var postedAt = new Date()
...

Protecting the admin controllers

controllers/Admin.scala
...
object Comments extends Controller with CRUDFor[Comment] with Secured
object Posts extends Controller with CRUDFor[Post] with Secured
object Tags extends Controller with CRUDFor[Tag] with Secured
object Users extends Controller with CRUDFor[User] with Secured
...

Customizing the authentication process

controllers/Admin.scala
...
object Security extends Secure.Security {
private def authentify(username: String, password: String) = {
  User.connect(username, password) != null
  }
}
...

Refactoring the administration area

controllers/Admin.scala
package controllers
import play._
import play.mvc._
import play.data.validation._
import models._
object Admin extends Controller with Defaults with Secured {
    @Before
    def setConnectedUser{
        if(Secure.Security.isConnected()) {
            val user = User.find("byEmail", Secure.Security.connected()).first
            renderArgs += "user" -> user.fullname
        }   
}
    def index {
        render()
    }
}
controllers/Admin.scala
...
object Security extends Secure.Security {    
  ...    
  private def onDisconnected = Application.index
  private def onAuthenticated = Admin.index
}
...

Adding roles

controllers/Admin.scala
...
object Security extends Secure.Security {
    ...
    private def check(profile: String) = {
        profile match {
            case "admin" => User.find("byEmail", Secure.Security.connected).first.isAdmin
            case _ => false
        }
    }
    ...
}
...

controllers/Admin.scala 
...
@Check(Array("admin")) object Comments extends Controller with CRUDFor[Comment] with Secured
@Check(Array("admin")) object Posts extends Controller with CRUDFor[Post] with Secured
@Check(Array("admin")) object Tags extends Controller with CRUDFor[Tag] with Secured
@Check(Array("admin")) object Users extends Controller with CRUDFor[User] with Secured

Start with the user posts list

controllers/Admin.scala
...
object Admin extends Controller with Defaults with Secured {
    ...
    def index {
        val posts = Post.find("author.email", Secure.Security.connected()).fetch
        render(posts)
    }
...
controllers/Admin.scala
...
object Admin extends Controller with Defaults with Secured {
    ...
    def form() {
      render()
    }     
    def save() {
       // Not implemented yet
    }
...


controllers/Admin.scala
...
object Admin extends Controller with Defaults with Secured {
    ...
    def save(id: Long, title: String, content: String, tags: String) {
        var post: Post = null
        val author = User.find("byEmail", Secure.Security.connected()).first;
        post = new Post(author, title, content)
        // Set tags list
        tags.split("""\s+""") foreach { tag: String =>
            if(tag.trim().length > 0) {
                post.tags add Tag.findOrCreateByName(tag)
            }
        }
        // Validate
        validation.valid(post)
        if(Validation.hasErrors()) {
            render("@form", post)
        }
        // Save
        post.save()
        index
    }
...

Reusing this stuff for Post 'edition'

controllers/Admin.scala
...
object Admin extends Controller with Defaults with Secured {
    ...
    def form(id: Long) {
        if(id != 0) {
            val post = Post.findById(id)
            render(post)
        }
        render()
    }
...

controllers/Admin.scala
...
object Admin extends Controller with Defaults with Secured {
    ...
    def save(id: Long, title: String, content: String, tags: String) {
      var post: Post = null
   if(id == 0) {
// Create posy
  val author = User.find("byEmail", Secure.Security.connected()).first;
post = new Post(author, title, content)
} else {
// Retrieve post
  post = Post.findById(id)
post.title = title
post.content = content
post.tags.clear()
}

   // Set tags list
   tags.split("""\s+""") foreach { tag: String =>
   if(tag.trim().length > 0) {
      post.tags add Tag.findOrCreateByName(tag)
     }
   }
...
 }
...

Testing the controller part

test/ApplicationTest.scala
import org.junit._
import play.test._
import play.mvc._
import play.mvc.Http._
import models._
import play.test.FunctionalTest._
class ApplicationTest extends FunctionalTest {
  @Test
  def testThatIndexPageWorks() {
    var response = GET("/")
    assertIsOk(response)
    assertContentType("text/html", response)
    assertCharset("utf-8", response)
  }
  @Test
  def testAdminSecurity() {
    var response = GET("/admin")
    assertStatus(302, response)
    assertHeaderEquals("Location", "http://localhost/login", response)
 }
}
Comments