Objectives

Incorporate Sessions into the latest Playlist Application

Accounts + Members

This lab assumes you have completed Lab-09b: Playlist-4. If you are unsure - you can download this completed version:

The Todolist application evolved Member model + an Accounts controller that we used to manage signup/login features. These are the classes from the Todo appkication here:

Accounts

package controllers;

import models.Member;
import play.Logger;
import play.mvc.Controller;

public class Accounts extends Controller
{
  public static void signup()
  {
    render("signup.html");
  }

  public static void login()
  {
    render("login.html");
  }

  public static void register(String firstname, String lastname, String email, String password)
  {
    Logger.info("Registering new user " + email);
    Member member = new Member(firstname, lastname, email, password);
    member.save();
    redirect("/");
  }

  public static void authenticate(String email, String password)
  {
    Logger.info("Attempting to authenticate with " + email + ":" + password);

    Member member = Member.findByEmail(email);
    if ((member != null) && (member.checkPassword(password) == true)) {
      Logger.info("Authentication successful");
      session.put("logged_in_Memberid", member.id);
      redirect ("/dashboard");
    } else {
      Logger.info("Authentication failed");
      redirect("/login");
    }
  }

  public static void logout()
  {
    session.clear();
    redirect ("/");
  }

  public static Member getLoggedInMember()
  {
    Member member = null;
    if (session.contains("logged_in_Memberid")) {
      String memberId = session.get("logged_in_Memberid");
      member = Member.findById(Long.parseLong(memberId));
    } else {
      login();
    }
    return member;
  }
}

Member

package models;

import play.db.jpa.Model;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Member extends Model
{
  public String firstname;
  public String lastname;
  public String email;
  public String password;

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

  public Member(String firstname, String lastname, String email, String password)
  {
    this.firstname = firstname;
    this.lastname = lastname;
    this.email = email;
    this.password = password;
  }

  public static Member findByEmail(String email)
  {
    return find("email", email).first();
  }

  public boolean checkPassword(String password)
  {
    return this.password.equals(password);
  }
}

Bring these two classes into your Playlist application now. The Member class will have an error:

  public List<Todo> todolist = new ArrayList<Todo>();

This is a holdover from the Todo application. Replace the above line with this:

  public List<Playlist> playlists = new ArrayList<Playlist>();

Each member will have a list of Playlist objects, called playlists. This is the members personal collection of playlists.

The project should now be without errors:

Signup & Login Forms + welcome Memu

Todolist also has signup and login views - which implement simple forms for this purpose:

views/signup.html

#{extends 'main.html' /}
#{set title:'Signup' /}
#{welcomemenu id:"signup"/}

<div class="ui two column grid basic middle aligned segment">
    <div class="column">
        <form class="ui stacked segment form" action="/register" method="POST">
            <h3 class="ui header">Register</h3>
            <div class="two fields">
                <div class="field">
                    <label>First Name</label>
                    <input placeholder="First Name" type="text" name="firstname">
                </div>
                <div class="field">
                    <label>Last Name</label>
                    <input placeholder="Last Name" type="text" name="lastname">
                </div>
            </div>
            <div class="field">
                <label>Email</label>
                <input placeholder="Email" type="text" name="email">
            </div>
            <div class="field">
                <label>Password</label>
                <input type="password" name="password">
            </div>
            <button class="ui blue submit button">Submit</button>
        </form>
    </div>
    <div class="column">
        <img class="ui image" src="/public/images/todo-1.png">
    </div>
</div>

views/login.html

#{extends 'main.html' /}
#{set title:'login' /}
#{welcomemenu id:"login"/}

<div class="ui two column middle aligned grid basic segment">
  <div class="column">
    <form class="ui stacked segment form" action="/authenticate" method="POST">
      <h3 class="ui header">Log-in</h3>
      <div class="field">
        <label>Email</label> <input placeholder="Email" name="email">
      </div>
      <div class="field">
        <label>Password</label> <input type="password" name="password">
      </div>
      <button class="ui blue submit button">Login</button>
    </form>
  </div>
  <div class="column">
    <img class="ui image" src="/public/images/todo-2.jpg">
  </div>
</div>

It also has this partial - a menu to support signup + login options:

views/tags/welcomemenu.html

<nav class="ui menu">
  <header class="ui header item"> <a href="#"> Playlist 5 </a></header>
  <div class="right menu">
    <a id="signup" class="item" href="/signup"> Signup  </a>
    <a id="login" class="item" href="/login">  Login   </a>
  </div>
</nav>

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

Bring this in now.

The existing menu.html will need an additional option to support log out:

views/tags/menu.html

<nav class="ui menu">
  <header class="ui header item"> <a href="#"> Playlist 5 </a></header>
  <div class="right menu">
    <a id="dashboard" class="item" href="/dashboard"> Dashboard  </a>
    <a id="about" class="item" href="/about"> About </a>
    <a id="logout" class="item" href="/logout"> Logout </a>
  </div>
</nav>

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

We should change the start view to include this menu instead of the main menu:

views/start.html

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

#{welcomemenu id:"start"/}

<section class="ui center aligned middle aligned segment">
  <h1 class="ui header">
    Welcome to Playlist 5
  </h1> 
  <p>
    A small app to let you compose playlists. This app will allow you to create, manage and share your playlists. Simple enter the playlist details one the dashboard.

    Please Signup or Login using the menu above.
  </p>
</section>

The view folder of the project should now look like this:

Routes

These forms require these additional routes:

conf/routes

GET    /signup                                  Accounts.signup
GET    /login                                   Accounts.login
POST   /register                                Accounts.register
POST   /authenticate                            Accounts.authenticate
GET    /logout                                  Accounts.logout

Make sure to place these additional route towards the top of the file:

# Home page
GET     /                                       Start.index

GET    /signup                                  Accounts.signup
GET    /login                                   Accounts.login
POST   /register                                Accounts.register
POST   /authenticate                            Accounts.authenticate
GET    /logout                                  Accounts.logout
...

Run the application now - and sign up 2 users and log in as each user in turn. Experiment with the various forms.

Do you notice the slightly odd behaviour? We seem to have a single playlist - regardless of who is logged in.

Clearly this is not what we intended - each user should have their own playlist, separate from other users. We will tackle this in the next step.

Sessions - Dashboard

This is the current version of the Dashboard controller:

package controllers;

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

import models.Playlist;
import models.Song;
import play.Logger;
import play.mvc.Controller;

public class Dashboard extends Controller
{
  public static void index() 
  {
    Logger.info("Rendering Admin");

    List<Playlist> playlists = Playlist.findAll();
    render ("dashboard.html", playlists);
  }

  public static void deletePlaylist (Long id)
  {
    Playlist playlist = Playlist.findById(id);
    Logger.info ("Removing" + playlist.title);
    playlist.delete();
    redirect ("/dashboard");
  }

  public static void addPlaylist (String title)
  {
    Playlist playlist = new Playlist (title, 0);
    Logger.info ("Adding a new playlist called " + title);
    playlist.save();
    redirect ("/dashboard");
  }
}

In the above, there is no reference to the currently logged in user - a single global collection of playlists is manipulated.

Replace this controller with the following version:

Dashboard.java

package controllers;

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

import models.Member;
import models.Playlist;
import models.Song;
import play.Logger;
import play.mvc.Controller;

public class Dashboard extends Controller
{
  public static void index() 
  {
    Logger.info("Rendering Dasboard");
    Member member = Accounts.getLoggedInMember();
    List<Playlist> playlists = member.playlists;
    render ("dashboard.html", playlists);
  }

  public static void deletePlaylist (Long id)
  {
    Logger.info("Deleting a Playlist");
    Member member = Accounts.getLoggedInMember();
    Playlist playlist = Playlist.findById(id);
    member.playlists.remove(playlist);
    member.save();
    playlist.delete();
    redirect ("/dashboard");
  }

  public static void addPlaylist (String title)
  {
    Logger.info("Adding a Playlist");
    Member member = Accounts.getLoggedInMember();
    Playlist playlist = new Playlist (title, 0);
    member.playlists.add(playlist);
    member.save();
    redirect ("/dashboard");
  }
}

This is modelled on the equivalent controller in the Todolist-2 application. Each controller method always carries out the following:

  • Log a suitable message
  • Get the currently logged in member
  • Manipulate that members playlists

Run the application now again, and verify that you can create and delete playlists. Keep a note of the names of the playlists you create for a given user - and switch users a few times. Verify that you are only manipulating the playlists for the logged in user.

Sessions - PlaylistCtrl

This is the current version of PlaylistCtrl

PlaylistCtrl

public class PlaylistCtrl extends Controller
{
  public static void index(Long id)
  {
    Playlist playlist = Playlist.findById(id);
    Logger.info ("Playlist id = " + id);
    render("playlist.html", playlist);
  }

  public static void deletesong (Long id, Long songid)
  {
    Playlist playlist = Playlist.findById(id);
    Song song = Song.findById(songid);
    Logger.info ("Removing" + song.title);
    playlist.songs.remove(song);
    playlist.save();
    song.delete();
    render("playlist.html", playlist);
  }

  public static void addSong(Long id, String title, String artist, int duration)
  {
    Song song = new Song(title, artist, duration);
    Playlist playlist = Playlist.findById(id);
    playlist.songs.add(song);
    playlist.save();
    redirect ("/playlists/" + id);
  }
}

Without making any changes to this class, log in as 2 different users, and populate a few playlists for each. Verify that when you switch users that the playlists are always appropriate to the logged in user.

Consider the following questions:

  • (1) Why do we not need a reference to the currently logged in user in this class?
  • (2) Could we trigger a route leading to one of these methods without logging in at all?
  • (3) If (2) is possible, how could we prevent this happening?

Model Seeding I

This is our current data.yml file:

Song(s1):
  title: Piano Sonata No. 3
  artist: Beethoven
  duration: 5
Song(s2):
  title: Piano Sonata No. 7
  artist: Beethoven
  duration: 6
Song(s3):
  title: Piano Sonata No. 10
  artist: Beethoven
  duration: 8  
Song(s4):
  title: Piano Concerto No. 27
  artist: Beethoven
  duration: 8
Song(s5):
  title: Piano Concertos No. 17
  artist: Beethoven
Song(s6):
  title: Piano Concerto No. 10
  artist: Beethoven
  duration: 12    
Song(s7):
  title: Opus 34 Six variations on a theme in F major
  artist: Beethoven
Song(s8):
  title: Opus 120 Thirty-three variations on a waltz by Diabelli in C major
  artist: Beethoven

Playlist(p1):
  title: Bethoven Sonatas
  duration: 19
  songs:
  - s1
  - s2
  - s3

Playlist(p2):
  title: Bethoven Concertos
  duration: 23
  songs:
  - s4
  - s5
  - s6  

Playlist(p3):
  title: Beethoven Variations
  duration: 26
  songs:
  - s7
  - s8

The contents of this file are still quite valid, but we can extend it to include members as well.

Append the following to the end of the file:

Member(m1):
  firstname: homer
  lastname: simpson
  email: homer@simpson.com
  password: secret

Member(m2):
  firstname: marge
  lastname: simpson
  email: marge@simpson.com
  password: secret

Restart the app - and verify that you can log in these users without signing up.

Model Seeding II

Revise the homer entry in the yml file:

Member(m1):
  firstname: homer
  lastname: simpson
  email: homer@simpson.com
  password: secret
  playlists:
  - p1
  - p2

Restart the app - and verify that when you log in homer has the contents of playlists p1 and p2

Solution

Archive of the project so far:

Exercise 1:

In the yml file, add playlist p3 to the marge user. Verify (by restarting) that this has worked.

Exercise 2:

Add some more songs, playlists and users to the yml file. Verify they are loaded (after an application restart)

Exercise 3:

If you look at the Signup or Login views in the app:

Notice there seems to be a missing image on the right. Try to ensure that some stock image appears here.

HINTL: Review Todolist 2, which has a solution to this issue.