Incorporate sessions tracking into the todo app
The starting point for this lab is the solutions to the Todo List app from the last lab. You will need to have completed these exercises in order to proceed.
If you still have some exercises not completed, then perhaps download this solution here:
and import into Idea as we have described in earlier labs. This solution includes:
Review each of these exercises, and make sure you can follow the essentials of the implementation.
Assuming you are working from the sample solution - or your own completed todolist - we will start with the View.
Replace the existing menu partial with the following:
<nav class="ui menu">
<header class="ui header item"> <a href="#"> Todo List </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>
Introduce this new menu alongside the existing one:
<nav class="ui menu">
<header class="ui header item"> <a href="#"> Todo List </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 in these new views:
#{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>
#{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>
Finally for this step, change one line in the start.html
view, from this:
...
#{menu id:"start"/}
...
to this:
...
#{welcomemenu id:"start"/}
...
Save everything now, and make sure that the app launches:
The app should start with a new menu:
However, the menu items will not work yet (try them).
Introduce the following new routes into the conf/routes file:
GET /signup Accounts.signup
GET /login Accounts.login
These routes lead to an accounts controller, which we can now bring in:
package controllers;
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");
}
}
Restart the app now, and you the signup/login forms should be rendered:
The submit buttons will not work yet however (try them).
Notice we seem to have missing images in the above. Download these two images here:
Place them in the public/images
folder in your project. If this goes correctly, the views should look like this:
We need a model to represent users (we will call them members) signed up to our application. Here is the class:
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);
}
}
Look carefully at this class. It is relatively simple, except we see each menber has a OneToMany
relationship to our existing todo class:
@OneToMany(cascade = CascadeType.ALL)
public List<Todo> todolist = new ArrayList<Todo>();
We already have a data.yml
file containing some todos:
Todo(t1):
title: Make tea
Todo(t2):
title: Go for snooze
Todo(t3):
title: Make more tea
We can extend this now, with some member objects:
Todo(t1):
title: Make tea
Todo(t2):
title: Go for snooze
Todo(t3):
title: Make more tea
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 now - and browse to the database:
When you connect, you should be able to see the Members table containing the users above:
You should make sure you can view the above. If you cannot, it may be:
Before proceeding, try to work out a procedure for reliably and consistently connecting to the database.
One last small change. Occasionally you will see an error from the app related to the database, particularly when you are making some small adjustments to the app, but without restarting
Here is a small change to Bootstrap.java
which will fix this problem:
import java.util.List;
import play.*;
import play.jobs.*;
import play.test.*;
import models.*;
@OnApplicationStart
public class Bootstrap extends Job
{
public void doJob()
{
if (Member.count() == 0)
{
Fixtures.loadModels("data.yml");
}
}
}
Review our signup form, and notice the action the form triggers:
...
<form class="ui stacked segment form" action="/register" method="POST">
...
We now implement this route:
POST /register Accounts.register
and here is the implementation of the matching action in the Accounts
controller:
...
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("/");
}
...
Restart the app now - and try the signup form. Register a new Member. Explore the database to see if the Member is actually created in the members table.
Now for the login form - a new route:
POST /authenticate Accounts.authenticate
...
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");
}
}
...
Restart the app now, and try to log in. If you try with one of the preloaded user details, you should be in to the dashboard.
If you try to logout, you get a 404. We can now implement this as well:
GET /logout Accounts.logout
...
public static void logout()
{
session.clear();
redirect ("/");
}
...
We should now be able to log in and log out.
Try the following:
You might notice that the same todos are presented to each member. Clearly there is something wrong here - we should be segmenting them per user.
To complete the app, a new method in the Accounts controller:
...
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;
}
...
The Dashoard index method can now be updated:
...
public static void index()
{
Logger.info("Rendering Dashboard");
Member member = Accounts.getLoggedInMember();
List<Todo> todolist = member.todolist;
render("dashboard.html", member, todolist);
}
...
Notice how we are changing how we retrieve the todos. Also, we are not passing member
as well as todolist
to the view.
We also need a new version of the addTodo
method:
public static void addTodo(String title)
{
Member member = Accounts.getLoggedInMember();
Todo todo = new Todo(title);
member.todolist.add(todo);
member.save();
Logger.info("Adding Todo" + title);
redirect("/dashboard");
}
We can make use of the member
in the dashboard view, replacing the header with the following:
...
<header class="ui header">
${member.firstname} ${member.lastname}'s Todo List
</header>
...
Restart the app now - and as different member, create some todos. Make sure that each member only sees his/her own todos.
We might also change the data.yml file to associate the todos with the members:
Todo(t1):
title: Make tea
Todo(t2):
title: Go for snooze
Todo(t3):
title: Make more tea
Member(m1):
firstname: homer
lastname: simpson
email: homer@simpson.com
password: secret
todolist:
- t1
- t2
Member(m2):
firstname: marge
lastname: simpson
email: marge@simpson.com
password: secret
todolist:
- t3
Restart the app, and verify that the indicated todos are visible when the user logs in.
Delete a todo will cause an error:
We will need to rethink how we do the todo delete method.
The main problem is that any given todo now 'belongs' to a member - so just deleting it will cause a problem for the 'integrity' of the database.
Here is a revised version of the method:
...
public static void deleteTodo(Long id, Long todoid)
{
Member member = Member.findById(id);
Todo todo = Todo.findById(todoid);
member.todolist.remove(todo);
member.save();
todo.delete();
Logger.info("Deleting " + todo.title);
redirect("/dashboard");
}
...
This version is different:
For this to work, we need this revised route:
GET /dashboard/{id}/deletetodo/{todoid} Dashboard.deleteTodo
(replace the existing deleteTodo route)
We also need a completely revised button in the dashboard:
<td> <a href="/dashboard/${member.id}/deletetodo/${todo.id} " class="ui tiny red button"> Delete </a> </td>
Restart the app - and verify that the todos can now be deleted successfully.
Archive of the project so far:
The developer tools in chrome allow you to see in detail the workings of the browser. In particular, under Application->Cookies
we can see the cookie our application has created:
If we log out, it will be deleted:
Experiment with locating the cookie - try to delete it manually, and see what effect it has on the app.
The developer tools can also reveal the cookie in transit during a request:
See if you can locate the above view in Chrome.
This is the final playlist project:
Using this, or your own completed solution from lab 09, try to incorporate the session
support we have just implemented in todo into the playlist app.
Essentially, we would like only members to be able to use the playlist app, and the members playlists are kept separate from other members.