The goal of the second project is to allow you to practice build web applications and databases in Python. Therefore, this project requires you to use Tornado and SQLite to create a music database.
You are to work in your Raspberry Pi groups and your code should run on the provided machines (although you can do your development anywhere and using any resources you wish).
Once again, to keep track of your work and to facilitate collaboration, your group will use your Projects repository on Bitbucket. The starter code for the project has already been merged into your Projects repository1. You simply need to do a git pull on your local clone to get the latest files.
After you have the files, you can see that the searchit
project as the following layout:
searchit \_ assets \_ html \_ album.html SearchIt Album Template \_ base.html SearchIt Base Template \_ gallery.html SearchIt Gallery Template \_ index.html SearchIt Index Template \_ track.html SearchIt Track Template \_ yaml \_ data.yaml Music data YAML \_ searchit \_ __init__.py \_ __main__.py Package Main Execution Module \_ database.py SearchIt Database Module \_ web.py SearchIt Web Module
For this project, you need to modify the following files:
data.yaml
: This is the source of the data for our database.
album.html
: This should display a table consisting of the tracks found in
the album.
gallery.html
: This should display a series of thumbnails for the items
found in the group.
index.html
: This should display your group name, group members, and group
logo.
track.html
: This should display information about the track.
database.py
: This implements the SQLite database.
web.py
: This implements the Tornado web application.
Each of these files has a TODO
marker that identifies what needs to be
implemented. Further details on what needs to be done are provided below.
The order in which you implement the different components is up to you. My recommendation is to get the database created and loaded and then implement and test one selection at a time.
The information for our database is stored in the file
assets/yaml/data.yaml
. As you can see, it is in YAML format, which is a
distant cousin to the JSON format we have discussed previously. The data
file looks like this:
- name: Taylor Swift image: https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Taylor_Swift_Red_Tour_2%2C_2013.jpg/800px-Taylor_Swift_Red_Tour_2%2C_2013.jpg albums: - name: Red image: https://upload.wikimedia.org/wikipedia/en/thumb/e/e8/Taylor_Swift_-_Red.png/220px-Taylor_Swift_-_Red.png tracks: - State of Grace - Red - Treacherous - I Knew You Were Trouble - All Too Well - 22 - I Almost Do - We Are Never Ever Getting Back Together - Stay Stay Stay - The Last Time - Holy Ground - Sad Beautiful Tragic - The Lucky One - Everything Has Changed - Starlight - Begin Again
Each member in the group should modify this file by adding at least one artist and at least one album. To find images, I simply used the Wikipedia entries for the respective artists and albums.
The database code can be found in searchit/database.py
. To complete this
module, you will need finish the searchit.database.Database
class by
implementing the following methods:
def __init__(self, data=YAML_PATH, path=SQLITE_PATH): # TODO: Set instance variables and call _create_tables and _load_tables self.logger = logging.getLogger() self.data = None self.path = None self.conn = None
The constructor requires you to set the data
, path
, and conn
instance variables appropriately. Likewise, you need to call the
_create_tables()
and _load_tables()
methods to create and load the
database tables when the database instance is created.
def _create_tables(self): # TODO: Create Artist, Album, and Track tables pass
The _create_tables
method should create the following tables:
Artists: Columns: ArtistID | Name | Image Primary Key: ArtistID Albums: Columns: ArtistID | AlbumID | Name | Image Primary Key: AlbumID Foreign Key: ArtistID Tracks: Columns: AlbumID | TrackID | Number | Name Primary Key: TrackID Foreign Key: AlbumID
ID
and other numerical columns should be INTEGER
while everything should
be TEXT
. None of the files should be NULL
.
def _load_tables(self): # TODO: Insert Artist, Album, and Track tables from YAML pass
The _load_tables
method should load the tables above by reading the
assets/yaml/data.yaml
file and inserting data into the appropriate
tables. A sketch of the code looks something like this:
for artist in yaml.load(open(self.data)): EXECUTE INSERT ARTIST SQL for album in artist['albums']: EXECUTE INSERT ALBUM SQL for track in album['tracks']: EXECUTE INSERT TRACK SQL
A few notes:
The YAML data structure contains a list of artists, which in turn consists of a dictionary with a list of albums, where each album contains a list of tracks.
The ArtistID
, AlbumID
, TrackID
, and Track Number
are stored in the
data file. Instead, you must keep counters for each field and increment or
reset them properly.
To help debug, you should use the logging facility by doing something like:
self.logger.debug('Added Artist: id={}, name={}'.format(artist_id, artist['name']))
def artists(self, artist): # TODO: Select artists matching query pass
The artists
method is used to search the Artists
table for any Names
that are similar to the artist
query. It should return the ArtistID
,
Name
, and Image
.
def artist(self, artist_id=None): # TODO: Select artist albums pass
The artist
method is used to retrieve the AlbumID
, Name
, and Image
of
the Albums
associated with the artist
specified by artist_id
.
def albums(self, album): # TODO: Select albums matching query pass
The albums
method is used to search the Albums
table for any Names
that
are similar to the album
query. It should return the AlbumID
, Name
,
and Image
.
def album(self, album_id=None): # TODO: Select specific album pass
The album
method is used to retrieve the TrackID
, Number
, and Name
of
the Tracks
associated with the album
specified by album_id
.
def tracks(self, track): # TODO: Select tracks matching query pass
The tracks
method is used to search the Tracks
table for any Names
that
are similar to the track
query. It should return the TrackID
, Name
,
and Albums.Image
. To retrieve the Image
associated with the track
you
will need to perform a JOIN
on the Albums
table.
def track(self, track_id=None): # TODO: Select specific track pass
The track
method is used to retrieve the TrackID
, Artists.ArtistID
,
Artists.Name
, Albums.AlbumID
, Albums.Name
, Number
, Tracks.Name
the
track
specified by track_id
. To retrieve all this information, you will
need to perform a JOIN
on the Albums
table and a JOIN
on the Artists
table.
The web application code can be found in searchit/web.py
. To complete this
module, you will need finish the searchit.web.Application
class and
complete the various Handler
classes:
def __init__(self, port=DEFAULT_PORT): tornado.web.Application.__init__(self, debug=True, template_path=Application.TEMPLATE_PATH) # TODO: Initialize database and port self.logger = logging.getLogger() self.ioloop = tornado.ioloop.IOLoop.instance() self.database = None self.port = None # TODO: Add Index, Artist, Album, and Track Handlers
For the Application
constructor, you need to set the database
and
port
instance variables. Additionally, you must add the handlers for
the IndexHandler
, ArtistHandler
, AlbumHandler
, and TrackHandler
classes.
class IndexHandler(tornado.web.RequestHandler): def get(self): # TODO: Implement Index Handler pass
The IndexHandler
should check the query
and table
arguments from the
HTML form.
If the table
is Artists
then it should call the artists
database
method on the query
to compute the list of matching artists.
If the table
is Albums
then it should call the albums
database method
on the query
to compute the list of matching albums.
If the table
is Tracks
then it should call the tracks
database method
on the query
to compute the list of matching tracks.
After this list of matches is computed, it should render the gallery.html
template with the appropriate arguments.
If no table is specified, then it should render the index.html
template
with the appropriate arguments.
class ArtistHandler(tornado.web.RequestHandler): def get(self, artist_id=None): # TODO: Implement Artist Handler pass
The ArtistHandler
should check if the artist_id
is valid. If so, then it
should call the artist
database method to retrieve a list of albums and
then render the gallery.html
template with appropriate arguments.
If artist_id
is not valid, then it should call the artists
database
method with an empty string to generate a list of all possible artists and
then render the gallery.html
template with the appropriate arguments.
class AlbumHandler(tornado.web.RequestHandler): def get(self, album_id=None): # TODO: Implement Album Handler pass
The AlbumHandler
should check if the album_id
is valid. If so, then it
should call the album
database method to retrieve a list of tracks then
render the album.html
template with appropriate arguments.
If album_id
is not valid, then it should call the albums
database
method with an empty string to generate a list of all possible albums and
then render the gallery.html
template with the appropriate arguments.
class TrackHandler(tornado.web.RequestHandler): def get(self, track_id=None): # TODO: Implement Album Handler pass
The TrackHandler
should check if the track_id
is valid. If so, then it
should call the track
database method to retrieve track information then
render the track.html
template with appropriate arguments.
If track_id
is not valid, then it should call the tracks
database
method with an empty string to generate a list of all possible tracks and
then render the gallery.html
template with the appropriate arguments.
Although the base.html
template is provided to you, you will need to edit
the following templates:
index.html
This should be modified to include: a list of the group members and your group logo.
album.html
This is used to display all the tracks in an album. This can be done with either a list or a table.
<table class="table table-striped"> <thead> <th>Number</th> <th>Name</th> </thead> <tbody> <tr> <td>$TRACK_NUMBER</td> <td><a href="/track/TRACK_ID">TRACK_NAME</a></td> </tr> ... <tr> <td>$TRACK_NUMBER</td> <td><a href="/track/TRACK_ID">TRACK_NAME</a></td> </tr> </tbody> </table>
gallery.html
This is used to display a series of thumbnail images.
<div class="row"> <div class="col-sm-3"> <a href="/PREFIX/ITEM_ID" class="thumbnail text-center"> <img src="ITEM_IMAGE"> <div class="caption"> <h4>ITEM_NAME</h4> </div> </a> </div> ... <div class="col-sm-3"> <a href="/PREFIX/ITEM_ID" class="thumbnail text-center"> <img src="ITEM_IMAGE"> <div class="caption"> <h4>ITEM_NAME</h4> </div> </a> </div> </div>
track.html
This is used to display the information amount a track.
<dl> <dt>Track ID</dt> <dd>TRACK_ID</dd> <dt>Artist</dt> <dd><a href="/artist/ARTIST_ID">ARTIST_NAME</a></dd> <dt>Album</dt> <dd><a href="/album/ALBUM_ID">ALBUM_NAME</a></dd> <dt>Track Number</dt> <dd>TRACK_NUMBER</dd> <dt>Track Name</dt> <dd>TRACK_NAME</dd> </dl>
base.html
The look of the web application is provided by the Bootstrap framework, which we will be learning after Spring Break. The particular theme is Flatly, which is part of the Bootswatch suite of themes.
Feel free to modify the base.html
to use other themes or to apply different
layouts.
To run web application, you can do the following:
$ python -m searchit
You can find an example of the web application at http://xavier.h4x0r.space:9876/.
To submit your project, please commit your work to your Projects repository by the beginning of class, Tuesday, March 15.
Charlie's and Sam's groups need to give Write permissions to repository. ↩