This repo contains a partially functioning version of the Ubermelon web app. While it is reasonably well constructed, there are a few bugs and unimplemented features, the repair of which will demonstrate some of the subtleties of working within a web framework.
The very first thing to do is clone this repository in the usual manner. Then, follow these steps to get set up.
- Create and activate a new, empty virtual environment.
- Use pip to install the packages required in requirements.txt (full tutorial on pip/virtualenv)
- Start the application by running
python melons.py
.
Navigate to http://localhost:5000 and start exploring the app.
Our application follows the model-view-controller pattern. Briefly, the app is divided into three sections, each with its own responsibility. The model is our database, its chief function is to store and retrieve data from long-term permanent storage.
The view of our application is also sometimes called the 'presentation layer'. This code is primarily concerned with how to display data back to the user. These are our .html template files.
Lastly, the controller is the mechanism that listens for data requests from the user, asks the model for the appropriate data (or to store new data), then delivers its findings to the view. Our controllers are found in the file melons.py
. In this case, our controllers also dictate which URLs are available to the user.
Because the controller is the piece that ties all of the components together, it will be our most important file. It will be the hub from which we explore the rest of the codebase. The gist of all of the above is that the code to generate any given page in our browser will be spread across three, or maybe even four files. However, it is straightforward enough to take a page and find the code that matches it.
- Get the URL of the page.
- Locate the function attached to the URL in
melons.py
. - Any model-related function calls will be made from this function, prefixed by the module
model
. (eg:model.get_melons()
) These functions are located inmodel.py
. - The template name (html file) will usually be located in the return statement for the function, as part of a
render_template()
call. - From the template, we can locate any .css files or images relevant to our problem.
Remember, each file has a specific responsibility, so even though you have more than one file open, it is straightforward enough to decide which file to be in.
Your first task is to explore the app from within your browser. The goal is to understand how the app is laid out. For each page, try to identify the following (and write it down somewhere)
- The controller function attached to each page
- The model function calls (if any) for a given page
- The template for a page
As you do this, watch out for a number of specific things.
Try to make sure you understand the source of all the variables that are being used in the .html templates. Whenever you see a {{ var }}
in a template, make sure that you can match the variable listed with one coming from your model.
You'll notice that the melon detail page uses an {% if %}
statement to display whether or not a given melon is seedless. This is a feature of the Jinja templating engine. In addition to giving us a mechanism for inserting placeholders into our HTML, it also gives us a few control structures, like if-statements and for-loops. We can use this to make the contents of our page a little more dynamic.
Jinja if-statement documentation
The template for our melon list has an example of a Jinja {% for %}
loop.
You'll see that some of the provided templates are very sparse. This is because for the most part, each page has the same HTML as all the other pages. In the spirit of not repeating ourselves, Jinja provides a mechanism to take the common parts of a template out and move it to a 'skeleton' template that is shared across pages. In our case, the skeleton is base.html
. As you fix various bugs, you may find that some of the bugs reside in the base template and not the page template.
Jinja template inheritance documentation
If you try to access the html templates directly in your browser, ie: navigate over to http://localhost:5000/templates/all_melons.html, you will get a 404 error. This is because templates have to be preprocessed before they can be displayed. They have to be run through a controller for all the placeholder variables to be replaced.
However, you can access http://localhost:5000/static/img/ubermelonsmall.png without any issue. This is because the ubermelon logo resides in a special directory called static
. Files that go in this directory are available to the browser without any preprocessing. This makes sense for files like images or stylesheets that don't change: static files.
Checking out our shopping cart is outside the scope of our task, but it is worth noting the 'message flash' mechanism that is used when you try to check out. It is a way to display one-time messages to the user. This is a great place to place error, warning, or success messages. Flashed messages only display once, if you reload the page, you will see that it disappears on the second view.
Flask Message Flashing documentation
Most of the style work in our app was not written manually. Instead, we used bootstrap, a css framework. The bootstrap framework specifies 'components', higher level UI components composed of more basic HTML tags. Notably, we use the navbar
component and the well
component.
If you click on the 'Log In' link in the nav bar at the top of the screen, you'll see that it does nothing. However, if you peruse the list of routes available in your melons.py
file, you will find that there is a login route that is inaccessible by clicking. You can, however, browse on over to the URL directly in your browser.
###Fixing this bug
-
Wire up the link to go to the page.
Your first task is to locate the
<a href>
tag that is used in the black bar at the top of the page. This section of the page is typically called the navbar. It may be tricky at first to find where in the code this tag exists. Remember to use the browser's element inspector to see exactly what lines of HTML you're looking for. Also remember that this part of the page is shared across multiple pages. -
Style the page.
When you do have the login page 'wired up', you'll notice that it's styled pretty terribly. We want it to use the same style as all the other pages. We could try to add bootstrap to the HTML directly, but it is easier to make this page a child of our
base.html
template. Check eithermelon_details.html
orall_melons.html
for an example on how to do that.
The melon cart link at the top of the page has a broken image. If you browse around our directory tree, you'll find that the file exists, but isn't linked properly.
###Fixing this bug
-
First, fix the link.
Make sure you understand why it's not displaying in the first place.
-
Style this component.
Find the stylesheet that's being used and fiddle with the style to make it display correctly. A height of 15px on this image should do it. Try to figure out a CSS selector that targets just that image without affecting others. Here's a css selector guide if you need help.
When you view the shopping cart, you'll notice that all the items in it are placeholder dummy items. We'll need to replace these items with actual melons. In addition, the 'Add to Cart' button on the melon detail page is wired up but the controller currently' doesn't do anything.
We need a way to temporarily hold information that the user generates (ie: which melons are in the cart). We could commit this to the database, but it would be cumbersome. Instead, we'll use a storage mechanism called 'the session' to carry information from clicking the 'Add to Cart' button all the way to the shopping cart page.
###Implementing this feature This feature is two-part. The order in which you build the feature doesn't matter, but it may be helpful to write both in conjunction.
-
Add things to the cart.
When you click the
add to cart
button, the fact that a melon has been added to a cart needs to be recorded somewhere. This isn't long-term information, nor is it information that's attached to any particular user. It's short-term information that's attached to the browser you're currently using. This kind of information is best stored in the session.For now, we can use a list as our 'cart' and just put our Melon objects inside it to represent the idea of them being added. Breaking down the process, here are the steps you should go through:
- On adding an item, check to see if the session contains a cart already
- If not, add a new cart (empty) list to the session
- Append the melon under consideration to our cart list
- Flash a message indicating the melon was successfully added to the cart
- Redirect the user to the shopping cart page
-
Display the cart contents.
Displaying the contents of the cart is a little simpler, but is an exercise in fiddling with Jinja templates and HTML tags. Essentially, in our shopping cart page, we will loop through all the melons in our cart and display them in a table.
- In our cart controller, get a list of melons in the cart out of the session (if it exists)
- Pass the list of melons on to the shopping cart template
- In the shopping cart template, delete the placeholder rows
- Use a jinja for loop to loop through the melon list, outputting each melon to the table in place of the original placeholders.
With each part of the feature being reasonably complex, it makes sense to do them in stages. For example, you might go through this sequence instead:
- On adding an item, add some arbitrary string to the session
- In the shopping cart page, add some code to display the string from the session.
- Going back to the
add_to_cart
controller, replace the string with the actual melon object - Going back to the shopping cart page, rewrite the page to display the single melon object
- In the
add_to_cart
controller, replace the single melon with a melon list...
And so on.
We'll implement a very simple login feature (that doesn't do anything) using all the same mechanisms we've seen until now. Exactly 'which user is logged in right now' is a good candidate for being stored in the session, as it's very browser-specific information. The general procedure is as follows
- When the user submits the login form, the email address and password get sent to the
process_login
controller. - From the
process_login
controller, we can extract the form fields from the request object. - We can use the submitted email to look up whether or not that user exists in the database (fill in the details of the
Customer
class and theget_customer_by_email
function). - If the customer exists, store the customer in the session. For this exercise, we will ignore whether or not the password matches.
- In our templates, if a customer is logged in (ie: they exist in the session), display their name and an option to log out instead of the login link.
- Flash a login successful message.
- Redirect the user to the main melon listings.