Welcome to part 2 of the Play Framework 2.3 Playful Prelaunch Tutorial. In the first tutorial we used Scala and the Play Framework to create static pages for a prelaunch or “Coming Soon” site. We used partials, Scala templates, Play Forms and specs2 for unit and integration testing. We also deployed our app to Heroku. My version of the app is located here.

In part 2 of the tutorial we are going to develop the following features:

  • Add the ability to collect the name and email address of someone interested in the Playful product.
  • We’re also going to send a follow up email to each person who signs up.
  • We’re also going to add internationalization to our product so that we can easily change the name of the product
  • Finally, we’re going to update our unit and integration tests to account for our new features.

Let’s start by adding internationalization to Play. Open your command line interface and go to the root of your my-playful-prelaunch project and type:

$ git checkout master
$ git checkout -b add-i18n

The first command is to make sure you started in the master branch. The second line creates the new branch in git. Now in your in /conf directory create a new text file called messages. In this file type or copy the following:

global.about = About us
global.appName = Playful Prelaunch App
global.companyName = Playful Prelaunch LLC.
global.how = How it works
global.welcomeToThe = Welcome to the
global.playFramework = Play! Framework

generic.thisIsThe = This is the
generic.sampleApp = Sample Application
generic.youCanFindWorking = You can find a working version of this app

We can now replace all of our text in our views and controllers with the variables on the right. Later if we want to change our app name, company name or any of these other words or phrases we only have to do it from here. When you’re launching a startup all of these things are in play (no pun intended). Let’s go to the following files and replace our text with the variables above. Keep in mind the following three things and you will successfully update your code:

  1. You’ll have to pass each variable to the Messages function. For example to use global.appName call Messages(“global.appName”)
  2. Add the following import function to your controllers:
    1. import play.api.i18n._
  3. In the scala views prepend the Messages function with an @ symbol if no other Scala code appears on that line. For example,
    1. <a class=”navbar-brand” href=”/”>@Messages(“global.appName”)</a>

Go ahead and make your changes to the following files:

  • /app/controllers/Application.scala
  • /app/controllers/Marketing.scala
  • /app/views/marketing/about.scala.html
  • /app/views/partials/header.scala.html
  • /app/views/index.scala.html
  • /app/views/main.scala.html

Now let’s rerun our tests from the CLI:

$ sbt test

If you have any compile issues be sure to read the compiler issues and compare them to the three points above. Finally, let’s update the following test files to use i18n rather than hard coding the strings. First add the “import play.api.i18n.Messages” statement to all 3 files and then change the hard coded references to “Playful Prelaunch App” and “About us”:

  • /test/ApplicationSpec.scala
  • /test/IntegrationSpec.scala
  • /test/MarketingSpec.scala

If we rerun our tests we should get all green. Now let’s commit to git:

$ git add --all
$ git commit -am "Add internationalization"
$ git checkout master
$ git merge add-i18n

At this point if you created a GitHub account as I suggested at the start you can push your changes to GitHub. This is also a good time to deploy your changes to Heroku.

$ git push github master
$ git push heroku master

We’ve accomplished quite a bit although very little of it is visible. Our next few changes will be substantial.

We’re going to create functionality that will allow visitors interested in subscribing to updates on our product to leave us their names and email addresses. Our registration page will look like this:

Playful_Prelaunch_Signup_Page

and a successful registration will look like this:
Playful_Prelaunch_Signup_Success_Page

We will start by by creating a new branch. In your CLI or IDE create a new branch:

$ git checkout -b registration-pages

Now let’s create our Registration controller by creating a new file called Registration.scala in /app/controllers and creating it as follows. :

package controllers
import play.api._
import play.api.mvc._
import play.api.i18n
object Registration extends Controller {
  //Will return a new registered Person model to populate
  def newReg = TODO
  //Will create a new registered Person model
  def create = TODO
}

The imports at the top of our controller are the same as our Marketing controller as is the object definition with the exception of the name. We’ve created the default actions we’ll implement for this controller:

  • newReg – Will return a person class suitable for the registration form
  • create – Will take a person object and create it

The TODO gives us a standard Not Implemented Yet page. Now that we have a template for our registration actions let’s update our routes. We will open our routes file and add the following after the Marketing section and before the map static resources section:

# Registration
GET    /register    controllers.Registration.newReg
POST   /register    controllers.Registration.create

Signup Todo
Our routes will now direct HTTP GET requests to /register to our new action and HTTP POST requests will go to our create action. If Play isn’t running go to the root of your my-playful-prelaunch and type:

$ activator run

Now open your browser and go to localhost:9000/register. You should see the following:
Signup Todo.

Now that we have our routes setup let’s propose our feature set for our registration pages, build tests based on these proposed features and then build out the features working our way from red to green.

Our registration page will reside at the root of our application and it will have three major features:

  • allow an interested visitor to give us her name and email address and submit the information to be saved by us.
  • redirect the person to a new page thanking her for her interest.
  • finally, the system will send her a thank you email.

Let’s start by updating our ApplicationSpec.scala to check that the registration page is located at the root of our application. Update the “render the index page” to look like this:

    
    "render the index/registration page" in new WithApplication{
      val home = route(FakeRequest(GET, "/")).get

      status(home) must equalTo(OK)
      contentType(home) must beSome.which(_ == "text/html")
      contentAsString(home) must contain (Messages("global.appName"))
      contentAsString(home) must contain (Messages("Register"))
      contentAsString(home) must contain (Messages("person.firstname"))
      contentAsString(home) must contain (Messages("person.lastname"))     
      contentAsString(home) must contain (Messages("email.email"))
    }

Let me break down each line of our code and explain what each accomplishes.

  • status(home) must equalTo(OK) -verifies that we received Http status 200 from our app.
  • contentType(home) must beSome.which(_ == “text/html”) – confirms that this route returned the content type “text/html” in the header
  • contentAsString(home) must contain (Messages(“

Now that we understand our test code let’s run our scripts and see the result:

$sbt test

As expected our new test has failed but let’s keep going and make sure we have a test for all of our functionality. Let’s open test/IntegrationSpec.scala and add the following after “work from within a browser”

"allow you to enter your name and email, register and take you to registrationSuccess page" in new WithBrowser {
      browser.goTo("http://localhost:" + port)

      val firstName: String = "Jane"
      val lastName: String = "Smith"
      val email: String = "jane@hawkinsunlimited.com"

      browser.$("#firstname").text(firstName)
      browser.$("#lastname").text(lastName)
      browser.$("#email").text(email)
      browser.$(".btn-primary").click()

      browser.pageSource must contain("Thank you for signing up to hear more")
      browser.pageSource must contain("Once we launch we will email you at " + email)
    }

The browser object we’re using here is provided by Selenium a tool to automate web browsers. Let’s run our test suite and confirm that we have two failed tests

$ sbt test

gives this result

[error] Error: Total 6, Failed 1, Errors 1, Passed 4
[error] Failed tests:
[error] 	ApplicationSpec
[error] Error during tests:
[error] 	IntegrationSpec
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 14 s, completed Aug 3, 2014 10:08:55 PM

We ran 6 tests, 4 passed, 1 failed and 1 returned an error. The error is because Selenium couldn’t get to the name element we specified in our test. Once we create our view with this element this error will be replaced with a failure until we complete the full spec. Now that we’ve finished our test suite let’s build the code that will pass our tests. First, let’s change our home route so it goes to Registration.newReg action rather than to the Application.index action.

# Home page
GET     /    controllers.Registration.newReg

At the same time comment out the Application.index action to make sure we don’t get any odd behavior. Now let’s update our Registration controller newReg action so it returns a NewPerson.

  def newReg = Action {
    Ok(views.html.registration.newReg(newPersonForm))
  }

This code is calling the newReg view (which is a function and can be called like a function because in Scala everything is a function) and passing it a value called newPersonForm. newPersonForm will allow our newReg form to return a populated person object via the controllers.Registration.create action to be inserted into our database. We’ll create the newPersonForm by adding the following to controllers.Registration just below the Registration object definition.

  val newPersonForm: Form[NewPerson] = Form(
    mapping(
      "firstName" -> nonEmptyText,
      "lastName" -> nonEmptyText,
      "email" -> nonEmptyText(minLength = 6) //shortest domain is k.st add @ + 1 letter and the min email length is 6
    )(NewPerson.apply)(NewPerson.unapply)
  )

Let’s break this code down line by line

  • The newPersonForm value is a Form type with a NewPerson type passed to it.
  • The Forms object we imported defines the mapping method that takes the elements of our form like “firstName” and constraints like “nonEmptyText” (meaning firstName must have some value).
  • The Forms object also takes an apply and an unapply function which we are fulfilling via a yet undefined NewPerson case class

We need to make one final update to our controller so that it is aware of the Forms object. We need to add the following just below our current block of imports in Registration.scala.

import models._
import play.api.data._
import play.api.data.Forms._

Now that we’ve set up our Registration controller for creating the empty Person form we need to create the NewPerson case class and new to support it. Start by creating a models directory under the app directory. Create a new file in the models directory called Person.scala. Now we need to add a block of code to support creating a new Person. Copy the following to Person.scala.

package models
import anorm._
import anorm.SqlParser._
import play.api.db._
import play.api.Play.current
import play.api.libs.ws._
import play.api.Play.current

case class NewPerson(firstName: String, lastName: String, email: String)
case class Person(id: Long, firstName: String, lastName: String, email: String)

object Person {

  val parser = {
    get[Long]("id") ~
      get[String]("firstname") ~
      get[String]("lastname") ~
      get[String]("email") map {
      case id ~ firstname ~ lastname ~ email => Person(id, firstname, lastname, email)
    }
  }
  def create(firstName: String, lastName: String, email: String): Person = {
       // TODO
  }
  def find(id: Long): Person = {
       // TODO
  }
}

As I said this may be a lot to digest. I left out the actual database access code because it is probably the most familiar and therefore least interesting of what we’ve done here. As we did before let’s go section by section and get an explanation of what all this does. Before we do that let me give you a hint. The NewPerson and Person case classes do all the translation between the form and the application. The Person object is responsible for CRUD.

  • The Form functions used to create personForm and newPersonForm in our Registration controller operate via these Person and NewPerson case classes which is why we import models._ at the top of the controller.
  • The parser does just as its name implies. It parses each field of the data we’ll get from SQL data store and uses it to create a Person object.
  • The create function takes firstname, lastname and email and returns a Person object using the parser.
  • The find function takes an id (specifically the auto-incrementing, primary key generated by our data store and returns the corresponding Person object.

Before we can create or return anything we need to create our database and tables. I use pgAdmin3 to manage my PostGres systems but use whatever tool you wish and create a database called my-playful-prelaunch-development. With our database created open your conf/application.conf, navigate to the Database configuration section (4 sections down) and enter the following:

db.default.driver=org.postgresql.Driver
db.default.url="jdbc:postgresql://localhost/my-playful-prelaunch-development"

We’ve now told my-playful-prelaunch about our database. Now we need to tell it to create our Person table. We’ll be using Evolutions to do this. Create a new folder under /conf called evolutions. Under this folder create a new folder called default. This new folder name must match the database name we used earlier in application.conf. The first database must be called default.

Let’s provide Play the SQL it will need to generate our person table. Create a new text file under conf/evolutions/default and name it 1.sql. Enter the following text into 1.sql

# --- Person schema

# --- !Ups

CREATE TABLE person (
    id bigserial NOT NULL,
    firstname varchar(255),
    lastname varchar(255),
    email varchar(255),
    CONSTRAINT person_pkey PRIMARY KEY (id)

);

# --- !Downs

DROP TABLE person;

As always let’s take our code section by section:

  • # — Person schema indicates what schema or schemas are modified in our script
  • # — !Ups indicates the transformations that need to occur. This delimiter must be here otherwise Play will not know what it should do with your script.
  • CREATE TABLE person … is the SQL we want Play to run to create our person table with fields matching the attributes of our case classes.
  • # — !Downs indicates how to reverse the !Ups transformations
  • DROP TABLE person; is the SQL we want Play to run to reverse our changes should we experience a problem.

I suggest you read the Play documentation on Evolutions to get a better understanding on how they work and how to control them. Pay special attention to the section on DOWN evolutions.

Now that we have a mechanism for creating our tables let’s update our Person object’s create and find functions.

  def create(firstName: String, lastName: String, email: String): Person = {
    DB.withConnection { implicit c =>
      val id: Long = SQL("INSERT INTO person(firstname, lastname, email) VALUES({firstname}, {lastname}, " +
        "{email})").on('firstname -> firstName, 'lastname -> lastName, 'email -> email)
        .executeInsert(scalar[Long] single)

      return Person.find(id)
    }
  }

  def find(id: Long): Person = {
    DB.withConnection{ implicit c =>
      SQL("SELECT id, firstname, lastname, email FROM person WHERE id = {id}").on('id -> id).using(Person.parser).single()
    }
  }

As before we will take each section and explain it. First, the create function:

  • DB is a helper provided by the import play.api.db._ and it gives us a connection to our PostGres database.
  • val id: Long uses the SQL function to insert a person into our person table.
  • The on function takes the arguments from the create function and puts into our placeholder braces.
  • executeInsert executes our SQL statement, returns our new Id from PostGres.
  • Finally, Person.find(id) is used to return the new person to our controller which uses it to greet our recently registered visitor.

Now for the find function:

  • As we might expect the DB helper with its connection makes another appearance.
  • As does the SQL and on function but with a SELECT statement to retrieve a person and the id from the find function arguments.
  • The function using is called with the Person.parser value so that we return an easily consumed Person object.
  • Finally, the single function only returns a single row.

We have two more things to complete to finish part 2 of the tutorial. We need a form to accept the visitor’s name and email address and we need to update our unit and functional tests. Let’s start by creating our registration form and our signup success forms. Create a folder in app/views called registration and inside of it create two new files called newReg.scala.html and registrationSuccess.scala.html. In the newReg.scala.html type the following:


@import helper._

@(newPersonForm: Form[NewPerson])

@main(Messages("global.welcomeToThe") + " " + Messages("global.appName") + " " + Messages("global.exampleapp")) {
    <div class="container">
        <div class="row">
            <div class="col-md-offset-1 col-lg-offset-1 col-md-4 col-lg-4">
                Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
            </div>
        </div>
        <div class="row">
            <div class="col-md-offset-1 col-md-10 col-lg-offset-1 col-lg-10">
                @helper.form(action = routes.Registration.create) {
                    <fieldset>
                        <h4>@Messages("signup.hearaboutlaunch")</h4>

                        
                        @inputText(
                            newPersonForm("firstName"), '_label -> Messages("person.firstname"),
                                'placeholder -> Messages("person.enterfirstname"), '_showErrors -> true, '_showConstraints -> true
                        )

                        @inputText(
                            newPersonForm("lastName"), '_label -> Messages("person.lastname"),
                            'placeholder -> Messages("person.enterlastname"), '_showErrors -> true,
                            '_showConstraints -> true
                        )
                        @inputText(
                            newPersonForm("email"), '_label -> Messages("email.email"),
                            'placeholder -> Messages("email.enteremail"), '_showErrors -> true,
                            '_help -> Messages("email.enteremail")
                        )
                    </fieldset>

                    <div class="actions">
                        <input type="submit" class="btn btn-primary" value=@Messages("signup.register")>
                    </div>
                }
            </div>
            <div></div>
        </div>
    </div>
}

As we’ve done before we will take each section and I will explain what it does, what it provides and why we need it (assuming I know. :D).

  • Let’s start with the @ sign. Scala templates borrow heavily from ASP.NET Razor and the @ indicates that the following text is Scala code.
  • @helper package contains the template helpers for Scala.
  • Scala templates are compiled as Scala functions. Functions have arguments and our newReg template requires a Form with a NewPerson case class. We can refresh our memory by looking at the val and newReg action in the Registration controller. We’ve now completed the circle.
  • If you recall app/views/main.scala.html is our template for the entire app. At the top it also defines a function that takes a title argument as a string and a content argument as html. Our @main(){} call is providing these two arguments.
  • Messages() is the function call for i18n that is pulling our strings from conf/messages.
  • @helper.form() is the appearance of our Scala forms helper that on compilation will be replaced with an HTML form.
  • @inputText is another Scala function that generates an HTML input tag.
  • _label -> Messages(“person.lastname”) assigns the HTML label Last Name to the input tag.
  • ‘placeholder -> Messages(“person.enterfirstname”) assigns the HTML placeholder attribute to the input tag.
  • ‘_showErrors -> true creates a dd tag for every violation of our field’s constraints and assigns each one a CSS class “error”.
  • ‘_showConstraints -> true creates additional dd tags for each our field’s constraints and assigns it a CSS class called “info”
  • ‘_help -> Messages(“address.enterzipcode”) creates a dd tag with the matching text and assigns it a CSS class called “info”

One thing to note. ‘_showConstraints -> true (or false) is mutually exclusive with ‘_help -> “Some help text” on a particular input tag. You can have one or the other but not both. And ‘_help is higher on the priority list so if you forget and use both then you will only see ‘_help.

Our newReg.scala.html can handle errors but what happens if everything goes to plan and our visitor enters the data we need in the format we need it? Right now we left our registration controllers create action as a todo. We will need to update this action to accept our person’s data and persist it to our database. Let’s open our registration controller and change our create action:

  def create = Action { implicit request =>
    newPersonForm.bindFromRequest.fold(
      errors => BadRequest(views.html.registration.newReg(errors)),
      person => {
        val newPerson = Person.create(person.firstName, person.lastName,
          person.email)
        Ok(views.html.registration.registrationSuccess(newPerson.id,
          newPerson.firstName, newPerson.lastName, newPerson.email))
      }
    )
  }

As always we’re going to dissect each section of our code.

  • def create = Action changes our create action to return an action of the todo message
  • implicit request =>. By default Play Actions receive a request and return a result. The request is the data we receive from our client. The implicit keyword allows us to pass the request on to other API functions without explicitly adding it to the argument list.
  • newPersonForm.bindFromRequest.fold() function handles pulling data from our request form and managing binding failures or binding success
  • errors => BadRequest(views.html.registration.newReg(errors)) is an example of a function with a function (views.html.registration.newReg) as an argument. In this particular case if bindFromRequest.fold fails to bind the request it calls BadRequest and passes the errors it received back to newReg. You should also be aware that BadRequest returns an HTTP status of 400.
  • person => … – is a multiline function so we have to wrap it in {} unlike errors which is not. The first line creates the value newPerson using the Person object’s create function. The next

successfully receive and persist our visitor’s information? We need some way to tell the person we have saved her information. Let’s build that now. Create a new form in app/views/registration called registrationSuccess.scala.html and enter the following code:

@(firstName: String, lastName: String, email: String)

@import helper._

@main(Messages("global.welcomeToThe") + " " + Messages("global.appName") + " " +
Messages("global.exampleapp")) {

    <div style="padding:50px">

        <p>Hello @firstName,</p>
        <p>Thank you for signing up to hear more about @Messages("global.appName").
          Once we launch we will email you at
            @email. </p>
        <p>Best regards,</p>
</p>
    </div>
}

We’ve seen all of this before.

  • The first line contains the arguments our form function will receive. We will only need the person’s first name and their email address.
  • As we’ve discussed before we imported the helper package for our templates
  • We’re calling the @main form function with two arguments: The page title as a string and the page body as html.

You may have noticed that we used the i18n Messages function quite a bit but we didn’t update our messages file. We will correct that now. Open conf/messages and add the following lines.

signup.signupHere = Sign up here
signup.hearaboutlaunch = Be the first to hear about our launch
signup.register = Register
signup.accountCreated = Your account has been created

person.firstname = First Name
person.lastname = Last Name
person.enterfirstname = Enter first name
person.enterlastname = Enter last name

contact.contactus = Contact us
contact.sendmessage = Send message
contact.comments = Comments

email.email = Email
email.enteremail = Enter email

address.zipcode = Zip Code
address.enterzipcode = Enter zip code

phone.phone = Phone number
phone.enterphone = Enter phone number

We are feature complete for step 2 of the tutorial but we have one 2 more functional test to write and 2 more for you to write on your own. If you recall we already adjusted our GET / route to point to Registration.newReg and we also updated our index unit test to account for adding the collection of a visitor’s first name, last name and email. What we have not done is test the functionality to make sure when someone enters the information correctly that it is persisted and we see the registrationSuccess form so let’s do that now. Open test/IntegrationSpec.scala and add the following section.

    "allow you to register your first name, last name and email and take you to registrationSuccess page" in new WithBrowser {
      browser.goTo("http://localhost:" + port)

      val firstName: String = "Jane"
      val lastName: String = "Smith"
      val email: String = "jane@hawkinsunlimited.com"

      browser.$("#firstName").text(firstName)
      browser.$("#lastName").text(lastName)
      browser.$("#email").text(email)
      browser.$(".btn-primary").click()

      browser.pageSource must contain("Hello " + firstName + ",")
      browser.pageSource must contain("Thank you for signing up to hear more")
      browser.pageSource must contain("Once we launch we will email you at " + email)
    }

As we have done before let’s go through our code section by section:

  • “allow you to register your first name, last name and email and take you to registrationSuccess page” in new WithBrowser { As before the quoted text is for us and describes what we’re testing. The new WithBrowser creates a new browser object.
  • browser.goTo(“http://localhost:” + port) uses our browser to navigate to our index.
  • val firstName: String, val lastName: String, and val email: String create the values we’re going to pass into our HTML inputs. Note they all end with : String so we know they are of String type.
  • browser.$(“#firstName”).text(firstName) uses the browser object we created at the outset to enter the firstName value from the previous section to the input with CSS id firstName. The same is true for #lastName and #email.
  • browser.$(“.btn-primary”).click() uses the browser object to call the click function on the object with CSS class btn-primary
  • The next three lines confirm that the registrationSuccess view is shown and that it has “Hello Jane”, “Thank you for signing up…” and the “Once we launch text…”. Notice that used our values again to make sure that our arguments are found in the new view.

Now if open your terminal and go to the root of my-playful-prelaunch and type

$ sbt test

you should get all green tests.

The last test we’re going to write together will confirm that if a visitor doesn’t enter a first name that our validation works. Add the following to the end of test/IntegrationSpec.scala:

    "fail to register you without your first name and prompt you with the missing information" in new WithBrowser {
      browser.goTo("http://localhost:" + port)

      val lastName: String = "Smith"
      val email: String = "jane@hawkinsunlimited.com"

      browser.$("#lastName").text(lastName)
      browser.$("#email").text(email)
      browser.$(".btn-primary").click()

      browser.pageSource must contain("This field is required")
    }

And again go to the terminal and run

$ sbt test

You should get all greens. This time you’ll notice we did not create a firstName value and more importantly we did not pass any value to browser.$(“#firstName”).text(firstName) and as a result our validation prevented the registration and told the visitor why.

If you’re using Heroku we have one final change to make. Open your Procfile and update it to look like so:

web: target/universal/stage/bin/my-playful-prelaunch -Dhttp.port=${PORT} -DapplyEvolutions.default=true -Ddb.default.url=${DATABASE_URL} -Ddb.default.driver=org.postgresql.Driver

Let’s break this down as before:

  • The applyEvolutions.default=true allows an environment perceived to be production like Heroku to run our evolutions and create our person table.
  • db.default.url=${DATABASE.URL} points our default database to Heroku’s DATABASE_URL environment variable.
  • db.default.driver defines the driver for our default database to be org.postgresql.Driver.

Let’s commit our code, merge it into master and then upgrade Heroku. Go to your terminal and type the following:

$ git status
$ git add --all
$ git commit -a -m "finish Playful Prelaunch part 2"
$ git push -u github registration-pages
$ git checkout master
$ git merge registration-pages
$ git push github master
$ git push heroku master
$ heroku open

Let’s do a quick rundown just in case you’ve forgotten Git or Heroku.

  • git status tells us the status of our local repository
  • git add –all tells git to add all files that are not in our git index (monitored and managed by git) to our git index and ready them (stage them) for a commit except those files that are in our .gitignore file
  • git commit -a -m “finish Playful Prelaunch part 2” commits our staged files whether new or modified
  • git push -u github registration-pages creates a remote “registration-pages” branch on GitHub that mirrors our local branch
  • git checkout master switches to our master branch where the only changes should be part 1 of our tutorial
  • git merge registration-pages merges our changes from the registration-pages branch (part 2) into the master branch (part 1). Consider master to be version 1.0 of our product and registration-pages to be version 2.0.
  • git push github master pushes our updated local master branch to its mirror on GitHub
  • git push heroku master pushes our updated local master branch to Heroku which unpacks it and installs it rather than storing it
  • heroku open launches your default browser and navigates to your instance of Playful Prelaunch

We’re done with part 2 of the Playful Prelaunch tutorial! I don’t know about you but I feel we accomplished a lot in this section. Next time we will add email and RESTful calls to the mix.

As always if you find an error, whether it’s a typo or an error in my understanding of Scala, Play or something else please let me know. You can reach me at info hawkinsunlimited [dot] com.