Move the playlists into their own view. Introduce routes for opening the playlist and also deleting individual songs.
Working only in the data.yml, create a new Playlist. If this works correctly, you should see the playlist in the dashboard just by restarting the application.
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 120 Thirty-three variations on a waltz by Diabelli in C 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
We would like an 'admin' interface to the application, which displays all the songs in the database - ignoring the playlists they belong to for the moment. The view should also display the IDs of the songs. This is a 'hidden' view, that does not appear in the menu. It is accessed by browsing directly to:
GET /admin Admin.index
#{extends 'main.html' /}
#{set title:'Dashboard' /}
#{menu id:"admin"/}
<section class="ui segment">
<h2 class="ui header">
All Known Songs in the Database:
</h2>
<table class="ui fixed table">
<thead>
<tr>
<th>ID </th>
<th>Song</th>
<th>Artist</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
#{list items:songs, as:'song'}
<tr>
<td>
${song.id}
</td>
<td>
${song.title}
</td>
<td>
${song.artist}
</td>
<td>
${song.duration}
</td>
</tr>
#{/list}
</tbody>
</table>
</section>
package controllers;
import java.util.List;
import models.Song;
import play.mvc.Controller;
public class Admin extends Controller
{
public static void index() {
List<Song> songs = Song.findAll();
render ("admin.html", songs);
}
}
We would like to change the app to just display a list of playlists on the dashboard, not the complete contents of each playlist. Replace the current dashboard with the following:
#{extends 'main.html' /}
#{set title:'Dashboard' /}
#{menu id:"dashboard"/}
#{list items:playlists, as:'playlist'}
<section class="ui segment">
<h2 class="ui header">
${playlist.title}
</h2>
<p> Total Duration: ${playlist.duration} </p>
<a href="#"> View </a>
</section>
#{/list}
This will render like this:
The view
links are currently inert (try them), but we would like them to cause a new view to be rendered, containing the playlist concerned.
As each playlist has an ID generated by the database, which can make this convenient to implement. Here is a new version of the view
link:
<a href="/playlists/${playlist.id}"> View </a>
With this change in place, try hovering over each view link (without pressing it). In Chrome, keep an eye on the status bar which should show a the link including the id:
Hover over each link and note how the ID changes. Clicking on any link causes the following error:
We need a new controller to display a new view containing the playlist details. We will do this in the next step.
The starting point for any new link in our app is to first define a route
to support this link. All supported routes are defined in config/conf
# Routes
# This file defines all application routes (Higher priority routes first)
#
# Home page
GET / Start.index
GET /dashboard Dashboard.index
GET /about About.index
GET /admin Admin.index
# Ignore favicon requests
GET /favicon.ico 404
# Map static resources from the /app/public folder to the /public path
GET /public/ staticDir:public
# Catch all
* /{controller}/{action} {controller}.{action}
In particular, these are the main routes currently supported:
GET / Start.index
GET /dashboard Dashboard.index
GET /about About.index
GET /admin Admin.index
These are the three patterns our app responds to:
/
/dashboard
/about
/admin
Any other pattern will generate a not found
error from our app.
Also note that each of these statements matches a route pattern with a function inside a controller. So, for instance, this route:
GET /about About.index
... ensures that this function would be called if the route was triggered:
public class About extends Controller
{
public static void index() {
Logger.info("Rendering about");
render ("about.html");
}
}
Reflect on this connection for a moment before proceeding.
We have a new pattern /playlist/id
, which we would like to route to a controller that would render a new view detailing the playlist contents:
...
<a href="/playlist/${playlist.id}"> View </a>
...
Supporting a new link link this usually requires three things:
Here is is the new controller:
package controllers;
import java.util.List;
import models.Playlist;
import models.Song;
import play.mvc.Controller;
public class PlaylistCtrl extends Controller
{
public static void index(Long id)
{
render("playlist.html");
}
}
This will render a view called playlist
. This is the playlist view (for the moment):
#{extends 'main.html' /}
#{set title:'Playlist' /}
#{menu id:"dashboard"/}
<section class="ui segment">
<h2 class="ui header">
Playlist details...
</h2>
</section>
Finally, the route:
GET /playlists/{id} PlaylistCtrl.index
Notice that the route includes this segment: {id}
. This means it matches any route that includes an extra wildcard segment at the end.
Implement all of the above now and verify that the view is rendered as expected. i.e. when we click on a 'View' link we see the above page.
In order to display the correct playlist, we need to extract the id from the url and then fetch the playlist from the model. Modify the playlist controller as follows:
...
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);
}
}
The id
from the route is passed as a parameter to the method. We use this id to get the correct playlist object:
Playlist playlist = Playlist.findById(id);
Logger.info ("Playlist id = " + id);
Run the app and select each of the playlist links in turn. The logs will display each of the Ids as you select the playlist. A different id should be logged for each.
Check the database to verify that these IDs are correct:
Occasionally, you may see an error like this:
When this occurs - restart the application (Ctrl-C and the play run
again).
Here is a revised version of the Playlist view:
#{extends 'main.html' /}
#{set title:'Playlist' /}
#{menu id:"dashboard"/}
<section class="ui segment">
<h2 class="ui header">
${playlist.title}
</h2>
<table class="ui table">
<thead>
<tr>
<th>Song</th>
<th>Artist</th>
</tr>
</thead>
<tbody>
#{list items:playlist.songs, as:'song'}
<tr>
<td>
${song.title}
</td>
<td>
${song.artist}
</td>
</tr>
#{/list}
</tbody>
</table>
</section>
Rerun the app now and verify that you can view each playlist. Check the database to confirm the IDs match.
We alrady have a listsongs.html
partial:
<table class="ui fixed table">
<thead>
<tr>
<th>Song</th>
<th>Artist</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
#{list items:_playlist.songs, as:'song'}
<tr>
<td>
${song.title}
</td>
<td>
${song.artist}
</td>
<td>
${song.duration}
</td>
</tr>
#{/list}
</tbody>
</table>
We could change the Playlist controler to call this to do the work of building the song table:
#{extends 'main.html' /}
#{set title:'Playlist' /}
#{menu id:"dashboard"/}
<section class="ui segment">
<h2 class="ui header">
${playlist.title}
</h2>
#{listsongs playlist:playlist /}
</section>
Verify that all of this works as expected.
This last step is a little challenging to understand fully - particularly how the listsongs.html partial is parameterised by the playlist passed from playlist.html. Look carefully at these templates for a minute and try to absorb the syntax.
Having a playlist app, without the ability to create/delete songs or playlists is clearly very limited. We have, essentially, an app that allows us to Read
our models, but not Create
, Update
or Delete
elements of the model.
We can start with providing a facility to delete songs from individual playlists. Our view will need to look like this:
and pressing the delete button should remove the corresponding song.
Any new button/link/action on our page requires:
.. and it may also involve some interaction with the model.
The new button must appear in each song row. Here is a revised listsongs partial:
<table class="ui fixed table">
<thead>
<tr>
<th>Song</th>
<th>Artist</th>
<th>Duration</th>
<th></th>
</tr>
</thead>
<tbody>
#{list items:_playlist.songs, as:'song'}
<tr>
<td>
${song.title}
</td>
<td>
${song.artist}
</td>
<td>
${song.duration}
</td>
<td>
<a href="/playlists/${_playlist.id}/deletesong/${song.id}" class="ui tiny red button">Delete Song</a>
</td>
</tr>
#{/list}
</tbody>
</table>
Look at the new element
<td>
<a href="/playlists/${_playlist.id}/deletesong/${song.id}" class="ui tiny red button">Delete Song</a>
</td>
It is just an <a>
element - styled to look like a button. The href
is manufactured to include the ids of both the playlist + the song. So it might be:
... href="/playlists/123/deletesong/321" ...
... where 123 is the id of the playlist, and 321 is the id of the song.
We now need a new route - containing both the playlist and song id - and linking to a new method in the playlist controller:
GET /playlists/{id}/deletesong/{songid} PlaylistCtrl.deleteSong
This is the new method to handle this route:
...
public static void deletesong (Long id, Long songid)
{
Playlist playlist = Playlist.findById(id);
Song song = Song.findById(songid);
Logger.info ("Removing" + song.title);
render("playlist.html", playlist);
}
...
Try all of this now - and verify that the logs shows the attempt to delete the song when the button is pressed.
We havent yet deleted the song - we will leave that to the next step.
To summarise, for every song we are generating a delete
button. This button's href contains the id of the playlist + the song we want to delete. When the user presses this button, it triggers a call to our new Controller method. This method is then able to locate the playlist/song in the database using the IDs from the route.
This is our deleteSong method:
public static void deletesong (Long id, Long songid)
{
Playlist playlist = Playlist.findById(id);
Song song = Song.findById(songid);
Logger.info ("Removing" + song.title);
render("playlist.html", playlist);
}
Note that we have 2 ids:
These ids are generated by this link:
<a href="/playlists/${playlist.id}/deletesong/${song.id}" class="ui tiny red button">Delete Song</a>
Annd are interpreted by this route:
GET /playlists/{id}/deletesong/{songid} PlaylistCtrl.deleteSong
Follow carefully the id
and songid
identifiers in the above.
To actually delete the song, we need to remove it from the playlist songs collection + delete from the database:
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);
}
Try this now - and make sure the songs are removed. Also, check that they are removed from the database table.
A complete version of the app as it should be at the end of this lab:
However, if you already have a project called 'playlist' in Idea, then it might be confusing So, first rename the project to playlist-2
(covered in step 1 of lab07b), and then run this command:
play idealize
... and then open with Idea.
Introduce a 'Delete Playlist' button for each playlist, represented by a trash
icon. E.g:
In addition, the view
link is replace by a folder open
icon. You can learn how to use these icons in Semantic UI here:
Bind the delete playlist
button to a new function to be implemented in the Dashboard controller, which should log the id of the playlist to be deleted.
Make the button actually delete the denoted playlist.