edits, tighten, standardize list format

This commit is contained in:
Bill Talcott 2023-02-23 15:30:54 -05:00
parent 88ed64b325
commit 86f35164a7

View File

@ -6,7 +6,7 @@
To start our journey into Hypermedia-Driven Applications, we are going to create a simple contact management web To start our journey into Hypermedia-Driven Applications, we are going to create a simple contact management web
application called Contact.app. We will start with a basic, "`Web 1.0-style`" Multi-Page Application (MPA), in the grand application called Contact.app. We will start with a basic, "`Web 1.0-style`" Multi-Page Application (MPA), in the grand
CRUD (Create, Read, Update, Delete) tradition. It will not be the best contact management application in the world. But CRUD (Create, Read, Update, Delete) tradition. It will not be the best contact management application in the world. But
that's OK because it will be simple (a great virtue of web 1.0 applications!) and it will do its job. that's OK because it will be simple and it will do its job.
This application will also be easy to incrementally improve in the coming chapter by utilizing the hypermedia-oriented This application will also be easy to incrementally improve in the coming chapter by utilizing the hypermedia-oriented
library htmx. library htmx.
@ -55,13 +55,13 @@ Rendering`" or SSR is emerging as the way that people talk about this style of t
as is common in SPA libraries. as is common in SPA libraries.
In Contact.app we will intentionally keep things as simple as possible to maximize the teaching value of our code: it In Contact.app we will intentionally keep things as simple as possible to maximize the teaching value of our code: it
won't be perfectly factored code, and it certainly won't be the most beautiful web application ever built, but it will won't be perfectly factored code, but it will
be easy to follow for readers even if they have little Python experience, and it should be easy to translate the application be easy to follow for readers even if they have little Python experience, and it should be easy to translate the application
and the techniques demonstrated into your preferred programming language and web framework. and the techniques demonstrated into your preferred programming language and web framework.
== Python == Python
Since this book is intended to teach how hypermedia can be used effectively, we aren't going to do deep dives into Since this book is intended to teach how hypermedia can be used effectively, we aren't going to do full tutorials for
the various technologies we use _around_ that hypermedia. This has some obvious drawbacks: if you aren't comfortable the various technologies we use _around_ that hypermedia. This has some obvious drawbacks: if you aren't comfortable
with Python, for example, some example python code in the book may be a bit confusing or mysterious at first. with Python, for example, some example python code in the book may be a bit confusing or mysterious at first.
@ -72,24 +72,21 @@ books/websites:
* https://learnpythonthehardway.org/python3/[Learn Python The Hard Way] by Zed Shaw * https://learnpythonthehardway.org/python3/[Learn Python The Hard Way] by Zed Shaw
* https://www.py4e.com/[Python For Everybody] by Dr. Charles R. Severance * https://www.py4e.com/[Python For Everybody] by Dr. Charles R. Severance
That being said, we think most web developers, even developers who are unfamiliar with Python, should be able to follow We think most web developers, even developers who are unfamiliar with Python, should be able to follow
along in the code. Most of the authors hadn't written very much Python before writing this book, and we got the hang of along in the code. Most of the authors hadn't written much Python before writing this book, and we got the hang of
it pretty quickly. it pretty quickly.
== Introducing Flask: Our First Route == Introducing Flask: Our First Route
Flask is a very simple but flexible web framework for Python. Just like this book isn't a Python book, it isn't a Flask book Flask is a very simple but flexible web framework for Python. It might be different than web frameworks
either, so we will only go into as much detail about it as is necessary to show off hypermedia concepts. However, unlike you are familiar with, so we will introduce its basic components.
Python, which is similar in many ways to other programming languages, Flask might be a bit different than web frameworks
you are familiar with, so we will need to do a bit more of an introduction to it in order to prepare you for the coming
chapters.
Thankfully, Flask is simple enough that most web developers shouldn't have a problem following along, so let's go over Thankfully, Flask is simple enough that most web developers shouldn't have a problem following along, so we'll just touch on its
the core ideas. the core elements.
A Flask application consists of a series of _routes_ tied to functions that execute when an HTTP request to a given path is A Flask application consists of a series of _routes_ tied to functions that execute when an HTTP request to a given path is
made. It uses a Python feature called "`decorators`" to declare the route that will be handled, which is then followed by made. It uses a Python feature called "`decorators`" to declare the route that will be handled, which is then followed by
a function to handle request to that route. We will often use the term "`handler`" to refer to the functions associated a function to handle requests to that route. We'll use the term "`handler`" to refer to the functions associated
with a route. with a route.
Let's create our first route definition, a simple "`Hello Flask`" route. In the following python code you will see the Let's create our first route definition, a simple "`Hello Flask`" route. In the following python code you will see the
@ -107,9 +104,9 @@ Here is what the code looks like:
def index(): <2> def index(): <2>
return "Hello World!" <3> return "Hello World!" <3>
---- ----
<1> Establishes we are mapping the `/` path as a route <1> Establishes we are mapping the `/` path as a route.
<2> The next method is the handler for that route <2> The next method is the handler for that route.
<3> Returns the string "`Hello World!`" to the client <3> Returns the string "`Hello World!`" to the client.
The `route()` method on the Flask decorator takes an argument: the path you wish the route to handle. Here we The `route()` method on the Flask decorator takes an argument: the path you wish the route to handle. Here we
pass in the root or `/` path, as a string, to handle requests to the root path. pass in the root or `/` path, as a string, to handle requests to the root path.
@ -125,8 +122,8 @@ application.
So we have the `index()` function immediately following our route definition for the root, and this will become the So we have the `index()` function immediately following our route definition for the root, and this will become the
handler for the root URL in our web application. handler for the root URL in our web application.
The handler in this case is dead simple, it just returns a string, "`Hello Flask!`", to the client. This isn't even The handler in this case is dead simple, it just returns a string, "`Hello Flask!`", to the client. This isn't
hypermedia yet, but, nonetheless, a browser will render it just fine: hypermedia yet, but a browser will render it just fine:
.Hello Flask! .Hello Flask!
image::figure_2-1_hello_world.png[Browser window, large text: Hello World!] image::figure_2-1_hello_world.png[Browser window, large text: Hello World!]
@ -155,39 +152,39 @@ def index():
<1> Update to a call to `redirect()` <1> Update to a call to `redirect()`
Now the `index()` function simply returns the result of calling the Flask-supplied `redirect()` function with the path Now the `index()` function returns the result of the Flask-supplied `redirect()` function with the path
we with to redirect the user to. In this case the path is `/contacts`, and we pass this path in as a string argument. we've supplied. In this case the path is `/contacts`, passed in as a string argument.
Now, if you navigate to the root path, `/`, our Flask application will forward you on to the `/contacts` path. Now, if you navigate to the root path, `/`, our Flask application will forward you on to the `/contacts` path.
== Contact.app Functionality == Contact.app Functionality
OK, now that we have our feet under us with respect to defining routes, let's get down to specifying and then implementing Now that we have some understanding of how to define routes, let's get down to specifying and then implementing
our web application. our web application.
What will Contact.app do? What will Contact.app do?
Initially, it will provide the following functionality: Initially, it will allow users to:
* Provide a list of contacts, including first name, last name, phone and email address * View a list of contacts, including first name, last name, phone and email address
* Provide the ability to search the list of contacts * Search the contacts
* Provide the ability to add a new contact to the list * Add a new contact
* Provide the ability to view the details of a contact on the list * View the details of a contact
* Provide the ability to edit the details of a contact on the list * Edit the details of a contact
* Provide the ability to delete a contact from the list * Delete a contact
So, as you can see, Contact.app is a fairly basic CRUD application, the sort of application that is perfect for an old-school So, as you can see, Contact.app is a CRUD application, the sort of application that is perfect for an old-school
web 1.0 approach. web 1.0 approach.
Note that the source code of Contact.app is available on https://github.com/bigskysoftware/contact-app[GitHub]. Note that the source code of Contact.app is available on https://github.com/bigskysoftware/contact-app[GitHub].
=== Showing A Searchable List Of Contacts === Showing A Searchable List Of Contacts
Let's look at our first real bit of functionality: the ability to show all the contacts in our system in a list (really, Let's add our first real bit of functionality: the ability to show all the contacts in our app in a list (really,
in a table). in a table).
This functionality is going to be found at the `/contacts` path, which is the path our previous route is redirecting to. This functionality is going to be found at the `/contacts` path, which is the path our previous route is redirecting to.
We will use Flask to route the `/contacts` path to a handler function, `contacts()`. This function is going to do one of We will use Flask to route the `/contacts` path to a handler function, `contacts()`. This function will do one of
two things: two things:
* If there is a search term found in the request, it will filter down to only contacts matching that term * If there is a search term found in the request, it will filter down to only contacts matching that term
@ -211,16 +208,15 @@ def contacts():
contacts_set = Contact.all() <3> contacts_set = Contact.all() <3>
return render_template("index.html", contacts=contacts_set) <4> return render_template("index.html", contacts=contacts_set) <4>
---- ----
<1> Look for the query parameter named `q`, which stands for "`query`" <1> Look for the query parameter named `q`, which stands for "`query.`"
<2> If the parameter exists, call the `Contact.search()` function with it <2> If the parameter exists, call the `Contact.search()` function with it.
<3> If not, call the `Contact.all()` function <3> If not, call the `Contact.all()` function.
<4> pass the result to the `index.html` template to render to the client <4> Pass the result to the `index.html` template to render to the client.
We see the same sort of routing code we saw in our first example, but we have a more elaborate handler function. We see the same sort of routing code we saw in our first example, but we have a more elaborate handler function.
First, we check to see if a search query parameter named `q` is part of the request. First, we check to see if a search query parameter named `q` is part of the request.
Query Strings:: A "`query string`" is part of the URL specification, and you are probably familiar with this term, but Query Strings:: A "`query string`" is part of the URL specification. Here is an example URL with a query string in it:
for those who are not, let's review what it is. Here is an example URL with a query string in it:
`https://example.com/contacts?q=joe`. The query string is everything after the `?` and, you can see, it has a `https://example.com/contacts?q=joe`. The query string is everything after the `?` and, you can see, it has a
name-value pair format. In this URL, the query parameter `q` is set to the string value `joe`. In plain HTML, a name-value pair format. In this URL, the query parameter `q` is set to the string value `joe`. In plain HTML, a
query string can be included in a request either by being hardcoded in an anchor tag or, more dynamically, by query string can be included in a request either by being hardcoded in an anchor tag or, more dynamically, by
@ -231,28 +227,23 @@ To return to our Flask route, if a query parameter named `q` is found, we call o
If the query parameter is _not_ found, we simply get all contacts by invoking the `all()` method on the `Contact` object. If the query parameter is _not_ found, we simply get all contacts by invoking the `all()` method on the `Contact` object.
Finally, we then render a template, `index.html` that displays the given contacts, passing in the results of whichever Finally, we render a template, `index.html` that displays the given contacts, passing in the results of whichever
of these two functions we ended up calling. of these two functions we end up calling.
.A Note On The Contact Class .A Note On The Contact Class
**** ****
The `Contact` Python class being used above is obviously a very important part of our overall system. It is the "`domain The `Contact` Python class we're using is the "`domain
model`" or just "`model`" class for our application, providing the "`business logic`" around the management of Contacts. model`" or just "`model`" class for our application, providing the "`business logic`" around the management of Contacts.
However, the _implementation_ of the `Contact` class is not relevant to Contact.app as a Hypermedia-Driven Application,
so we will not be looking at the internals of the class.
It could be working with a database (it isn't) or a simple flat file (it is), but it doesn't really matter. We want It could be working with a database (it isn't) or a simple flat file (it is), but we're going skip over the internal details of the model. Think of it as a "`normal`" domain model class, with methods on it that act in a "`normal`" manner.
to leave that part of the system aside, and treat it as a black box. We will present contacts as a resource to our
Hypermedia-Driven front end, but not concern ourselves with the internal details of the model.
We ask you to simply accept that it is a "`normal`" domain model class, and the methods on it act in the "`normal`" manner. We will treat `Contact` as a _resource_, and focus on how to effectively provide hypermedia representations
We will focus on `Contact` as a _resource_ and how to effectively provide hypermedia representations
of that resource to clients. of that resource to clients.
**** ****
==== The List & Search Templates ==== The List & Search Templates
Now that we have our handler logic written, we need to take a look at the templates that we are going to use to render Now that we have our handler logic written, we need to look at the templates that we are going to use to render
HTML in our response to the client. At a high level, our HTML response needs to have the following elements: HTML in our response to the client. At a high level, our HTML response needs to have the following elements:
* A list of any matching or all contacts * A list of any matching or all contacts
@ -283,15 +274,15 @@ Let's look at the first few lines of code in the `index.html` template:
<input type="submit" value="Search"/> <input type="submit" value="Search"/>
</form> </form>
---- ----
<1> Set the layout template for this template <1> Set the layout template for this template.
<2> Delimit the content to be inserted into the layout <2> Delimit the content to be inserted into the layout.
<3> Create a search form that will issue an HTTP `GET` to `/contacts` <3> Create a search form that will issue an HTTP `GET` to `/contacts`.
<4> Create an input that a query can be typed into to search contacts <4> Create an input for a user to type search queries.
The first line of code references a base template, `layout.html`, with the `extends` directive. This layout The first line of code references a base template, `layout.html`, with the `extends` directive. This layout
template provides the layout for the page (again, sometimes called "`the chrome`"): it wraps the template content in an template provides the layout for the page (again, sometimes called "`the chrome`"): it wraps the template content in an
`<html>` tag, imports any necessary CSS and JavaScript in a `<head>` element, places a `<body>` tag around the main `<html>` tag, imports any necessary CSS and JavaScript in a `<head>` element, places a `<body>` tag around the main
content and so forth. All the common content that wrapped around the "`normal`" content for the entire application content and so forth. All the common content wrapped around the "`normal`" content for the entire application
is located in this file. is located in this file.
The next line of code declares the `content` section of this template. This content block is used by the `layout.html` The next line of code declares the `content` section of this template. This content block is used by the `layout.html`
@ -308,10 +299,10 @@ the search value when a user does a search, so that when the results of a search
the term that was searched for. This makes for a better user experience since the user can see exactly what the the term that was searched for. This makes for a better user experience since the user can see exactly what the
current results match, rather than having a blank text box at the top of the screen. current results match, rather than having a blank text box at the top of the screen.
Finally, we have a submit-type input. This will render as a button and, when it is clicked on, it will trigger the Finally, we have a submit-type input. This will render as a button and, when it is clicked, it will trigger the
form to issue an HTTP request. form to issue an HTTP request.
This search UI forms the top of our contact page. Following it is a table of contacts, either all contacts or the This search interface forms the top of our contact page. Following it is a table of contacts, either all contacts or the
contacts that match the search, if a search was done. contacts that match the search, if a search was done.
Here is what the template code for the contact table looks like: Here is what the template code for the contact table looks like:
@ -339,19 +330,19 @@ Here is what the template code for the contact table looks like:
</tbody> </tbody>
</table> </table>
---- ----
<1> Output some headers for our table <1> Output some headers for our table.
<2> Iterate over the contacts that were passed in to the template <2> Iterate over the contacts that were passed in to the template.
<3> Output the values of the current contact, first name, last name, etc. <3> Output the values of the current contact, first name, last name, etc.
<4> An "operations" column, with links to edit or view the contact details <4> An "operations" column, with links to edit or view the contact details.
This is the core of the page: we construct a table with appropriate headers matching the data we are going This is the core of the page: we construct a table with appropriate headers matching the data we are going
to show for each contact. We iterate over the contacts that were passed into the template by the handler method using to show for each contact. We iterate over the contacts that were passed into the template by the handler method using
the `for` loop directive in Jinja2. We then construct a series of rows, one for each contact, where we render the the `for` loop directive in Jinja2. We then construct a series of rows, one for each contact, where we render the
first and last name, phone and email of the contact as table cells in the row. first and last name, phone and email of the contact as table cells in the row.
Additionally, we have another table cell that includes two links: Additionally, we have a table cell that includes two links:
* A link to the "Edit" page for the contact, located at `/contacts/{{ contact.id }}/edit` (e.g. For the contact with * A link to the "Edit" page for the contact, located at `/contacts/{{ contact.id }}/edit` (e.g., For the contact with
id 42, the edit link will point to `/contacts/42/edit`) id 42, the edit link will point to `/contacts/42/edit`)
* A link to the "View" page for the contact `/contacts/{{ contact.id }}` (using our previous contact example, the view * A link to the "View" page for the contact `/contacts/{{ contact.id }}` (using our previous contact example, the view
@ -369,8 +360,8 @@ Finally, we have a bit of end-matter: a link to add a new contact and a Jinja2 d
{% endblock %} <2> {% endblock %} <2>
---- ----
<1> Link to the page that allows you to create a new contact <1> Link to the page that allows you to create a new contact.
<2> The closing element of the `content` block <2> The closing element of the `content` block.
And that's our complete template. Using this simple server-side template, in combination with our handler method, we And that's our complete template. Using this simple server-side template, in combination with our handler method, we
can respond with an HTML _representation_ of all the contacts requested. So far, so hypermedia. can respond with an HTML _representation_ of all the contacts requested. So far, so hypermedia.
@ -398,7 +389,7 @@ The next bit of functionality that we will add to our application is the ability
are going to need to handle that `/contacts/new` URL referenced in the "`Add Contact`" link above. Note that when a user are going to need to handle that `/contacts/new` URL referenced in the "`Add Contact`" link above. Note that when a user
clicks on that link, the browser will issue a `GET` request to the `/contacts/new` URL. clicks on that link, the browser will issue a `GET` request to the `/contacts/new` URL.
All the other routes we have been looking at so far are using `GET` as well, but we are actually going to use two All the other routes we have so far use `GET` as well, but we are actually going to use two
different HTTP methods for this bit of functionality: an HTTP `GET` to render a form for adding a new contact, different HTTP methods for this bit of functionality: an HTTP `GET` to render a form for adding a new contact,
and then an HTTP `POST` _to the same path_ to actually create the contact, so we are going to be explicit about the and then an HTTP `POST` _to the same path_ to actually create the contact, so we are going to be explicit about the
HTTP method we want to handle when we declare this route. HTTP method we want to handle when we declare this route.
@ -439,10 +430,10 @@ Here is what our HTML looks like:
<span class="error">{{ contact.errors['email'] }}</span> <4> <span class="error">{{ contact.errors['email'] }}</span> <4>
</p> </p>
---- ----
<1> A form that submits to the `/contacts/new` path, using an HTTP `POST` <1> A form that submits to the `/contacts/new` path, using an HTTP `POST`.
<2> A label for the first form input <2> A label for the first form input.
<3> the first form input, of type email <3> The first form input, of type email.
<4> Any error messages associated with this field <4> Any error messages associated with this field.
In the first line of code we create a form that will submit back _to the same path_ that we are handling: `/contacts/new`. In the first line of code we create a form that will submit back _to the same path_ that we are handling: `/contacts/new`.
Rather than issuing an HTTP `GET` to this path, however, we will issue an HTTP `POST` to it. Using a `POST` in this manner Rather than issuing an HTTP `GET` to this path, however, we will issue an HTTP `POST` to it. Using a `POST` in this manner
@ -488,19 +479,18 @@ Finally, we have a button that will submit the form, the end of the form tag, an
</p> </p>
---- ----
It is worth pointing out something that is easy to miss in this straight-forward example: we are seeing the flexibility Easy to miss in this straight-forward example: we are seeing the flexibility
of hypermedia in action. of hypermedia in action.
If we add a new field, remove a field, or change the logic around how fields are validated or work with one another, If we add a new field, remove a field, or change the logic around how fields are validated or work with one another,
this new state of affairs would be reflected in the new hypermedia representation given to users. A user would see the this new state of affairs would be reflected in the new hypermedia representation given to users. A user would see the
updated new form, and be able to work with whatever new features is had, with no software update required. updated new form, and be able to work with new features, with no software update required.
==== Handling The Post to `/contacts/new` ==== Handling The Post to `/contacts/new`
The next step in our application is to handle the `POST` that this form makes to `/contacts/new`. The next step in our application is to handle the `POST` that this form makes to `/contacts/new`.
To do so, we need to add another route to our application that handles the `/contacts/new` path like our handler above, To do so, we need to add another route to our application that handles the `/contacts/new` path. The new route will handle an HTTP `POST` method instead of an HTTP `GET`. We will use the submitted form values to attempt to
but that handles an HTTP `POST` method instead of an HTTP `GET`. We will use the submitted form values to attempt to
create a new Contact. create a new Contact.
If we are successful in creating a Contact, we will redirect the user to the list of contacts and show a success message. If we are successful in creating a Contact, we will redirect the user to the list of contacts and show a success message.
@ -522,13 +512,13 @@ def contacts_new():
else: else:
return render_template("new.html", contact=c) <4> return render_template("new.html", contact=c) <4>
---- ----
<1> We construct a new contact object with the values from the form <1> We construct a new contact object with the values from the form.
<2> We try to save it <2> We try to save it.
<3> On success, "`flash`" a success message & redirect to the `/contacts` page <3> On success, "`flash`" a success message & redirect to the `/contacts` page.
<4> On failure, re-render the form, showing any errors to the user <4> On failure, re-render the form, showing any errors to the user.
The logic in this handler is a bit more complex than other methods we have seen, but it isn't too bad. The first thing The logic in this handler is a bit more complex than other methods we have seen. The first thing
we do is create a new Contact, again using the `Contact()` syntax in Python to construct the object. We pass in the values we do is create a new Contact, again using the `Contact()` syntax in Python to construct the object. We pass in the values
that the user submitted in the form by using the `request.form` object, a feature provided by Flask. that the user submitted in the form by using the `request.form` object, a feature provided by Flask.
@ -537,14 +527,14 @@ name associated with the various inputs.
We also pass in `None` as the first value to the `Contact` constructor. This is the "`id`" parameter, and by passing in We also pass in `None` as the first value to the `Contact` constructor. This is the "`id`" parameter, and by passing in
`None` we are signaling that it is a new contact, and needs to have an ID generated for it. (Again, we are not `None` we are signaling that it is a new contact, and needs to have an ID generated for it. (Again, we are not
going to dig deeply into the details of how this model object is implemented, our only concern is using it to generate going into the details of how this model object is implemented, our only concern is using it to generate
hypermedia responses.) hypermedia responses.)
Next, we call the `save()` method on the Contact object. This method returns `true` if the save is successful, and `false` if Next, we call the `save()` method on the Contact object. This method returns `true` if the save is successful, and `false` if
the save is unsuccessful (for example, a bad email was submitted by the user) the save is unsuccessful (for example, a bad email was submitted by the user).
If we are able to save the contact (that is, there were no validation errors), we create a _flash_ message indicating If we are able to save the contact (that is, there were no validation errors), we create a _flash_ message indicating
success and redirect the browser back to the list page. A "`flash`" is a common feature in web frameworks that allows success, and redirect the browser back to the list page. A "`flash`" is a common feature in web frameworks that allows
you to store a message that will be available on the _next_ request, typically in a cookie or in a session store. you to store a message that will be available on the _next_ request, typically in a cookie or in a session store.
Finally, if we are unable to save the contact, we re-render the `new.html` template with the contact. This will show the Finally, if we are unable to save the contact, we re-render the `new.html` template with the contact. This will show the
@ -553,7 +543,7 @@ fields will be rendered to feedback to the user as to what validation failed.
.The Post/Redirect/Get Pattern .The Post/Redirect/Get Pattern
**** ****
This handler is implementing a very common strategy in web 1.0-style development called the This handler implements a common strategy in web 1.0-style development called the
https://en.wikipedia.org/wiki/Post/Redirect/Get[Post/Redirect/Get] or PRG pattern. By issuing an HTTP redirect once https://en.wikipedia.org/wiki/Post/Redirect/Get[Post/Redirect/Get] or PRG pattern. By issuing an HTTP redirect once
a contact has been created and forwarding the browser on to another location, we ensure that the `POST` does not a contact has been created and forwarding the browser on to another location, we ensure that the `POST` does not
end up in the browsers request cache. end up in the browsers request cache.
@ -566,8 +556,7 @@ We will use the PRG pattern in a few different places in this book.
**** ****
OK, so we have our server-side logic set up to save contacts. And, believe it or not, this is about as complicated as OK, so we have our server-side logic set up to save contacts. And, believe it or not, this is about as complicated as
our handler logic will get, even when we look at adding more sophisticated htmx-driven behaviors. Simplicity is a great our handler logic will get, even when we look at adding more sophisticated htmx-driven behaviors.
selling point for hypermedia!
=== Viewing The Details Of A Contact === Viewing The Details Of A Contact
@ -575,12 +564,12 @@ The next piece of functionality we will implement is the detail page for a Conta
page by clicking the "`View`" link in one of the rows in the list of contacts. This will take them to the path page by clicking the "`View`" link in one of the rows in the list of contacts. This will take them to the path
`/contact/<contact id>` (e.g. `/contacts/42`). `/contact/<contact id>` (e.g. `/contacts/42`).
Note that this is a common pattern in web development: Contacts are being treated as resources and the URLs around these This is a common pattern in web development: contacts are treated as resources and the URLs around these
resources are organized in a coherent manner: resources are organized in a coherent manner.
* If you wish to view all contacts, you issue a `GET` to `/contacts` * If you wish to view all contacts, you issue a `GET` to `/contacts`.
* If you wish to get a hypermedia representation allowing you to create a new contact, you issue a `GET` to `/contacts/new` * If you want a hypermedia representation allowing you to create a new contact, you issue a `GET` to `/contacts/new`.
* If you wish to view a specific contact (with, say, an id of `42), you issue a `GET` to `/contacts/42` * If you wish to view a specific contact (with, say, an id of `42), you issue a `GET` to `/contacts/42`.
.The Eternal Bike Shed of URL Design .The Eternal Bike Shed of URL Design
**** ****
@ -589,8 +578,8 @@ It is easy to quibble about the particulars of the path scheme you use for your
"`Should we `POST` to `/contacts/new` or to `/contacts`?`" "`Should we `POST` to `/contacts/new` or to `/contacts`?`"
We have seen many arguments online and in person advocating for one approach versus another. We feel it is more We have seen many arguments online and in person advocating for one approach versus another. We feel it is more
important to understand the overarching idea of _resources_ and the _hypermedia representations_, rather than important to understand the overarching idea of _resources_ and _hypermedia representations_, rather than
getting too worked up about the smaller details of your URL design. getting worked up about the smaller details of your URL design.
We recommend you just pick a reasonable, resource-oriented URL layout you like and then stay consistent. Remember, We recommend you just pick a reasonable, resource-oriented URL layout you like and then stay consistent. Remember,
in a hypermedia system, you can always change your end-points later, because you are using hypermedia as the engine in a hypermedia system, you can always change your end-points later, because you are using hypermedia as the engine
@ -611,16 +600,16 @@ def contacts_view(contact_id=0): <2>
contact = Contact.find(contact_id) <3> contact = Contact.find(contact_id) <3>
return render_template("show.html", contact=contact) <4> return render_template("show.html", contact=contact) <4>
---- ----
<1> Map the path, with a path variable named `contact_id` <1> Map the path, with a path variable named `contact_id`.
<2> The handler takes the value of this path parameter <2> The handler takes the value of this path parameter.
<3> Look up the corresponding contact <3> Look up the corresponding contact.
<4> Render the `show.html` template <4> Render the `show.html` template.
You can see the syntax for extracting values from the path in the first line of code: you enclose the part of the You can see the syntax for extracting values from the path in the first line of code: you enclose the part of the
path you wish to extract in `<>` and give it a name. This component of the path will be extracted and then passed path you wish to extract in `<>` and give it a name. This component of the path will be extracted and then passed
into the handler function, via the parameter with the same name. into the handler function, via the parameter with the same name.
So, if you were to navigate to the path `/contacts/42` then the value `42` would be passed into the `contacts_view()` So, if you were to navigate to the path `/contacts/42`, the value `42` would be passed into the `contacts_view()`
function for the value of `contact_id`. function for the value of `contact_id`.
Once we have the id of the contact we want to look up, we load it up using the `find` method on the `Contact` object. We Once we have the id of the contact we want to look up, we load it up using the `find` method on the `Contact` object. We
@ -629,7 +618,7 @@ then pass this contact into the `show.html` template and render a response.
=== The Contact Detail Template === The Contact Detail Template
Our `show.html` template is relatively simple, just showing the same information as the table but in a slightly different Our `show.html` template is relatively simple, just showing the same information as the table but in a slightly different
format (perhaps for printing). If we add functionality like "`notes`" to the application later on, however, this will give format (perhaps for printing). If we add functionality like "`notes`" to the application later on, this will give
us a good place to do so. us a good place to do so.
Again, we will omit the "`chrome`" of the template and focus on the meat: Again, we will omit the "`chrome`" of the template and focus on the meat:
@ -650,7 +639,7 @@ Again, we will omit the "`chrome`" of the template and focus on the meat:
</p> </p>
---- ----
We simply render a nice First Name and Last Name header, with the additional contact information below it, We simply render a First Name and Last Name header, with the additional contact information below it,
and a couple of links: a link to edit the contact and a link to navigate back to the full list of contacts. and a couple of links: a link to edit the contact and a link to navigate back to the full list of contacts.
=== Editing And Deleting A Contact === Editing And Deleting A Contact
@ -675,8 +664,8 @@ def contacts_edit_get(contact_id=0):
return render_template("edit.html", contact=contact) return render_template("edit.html", contact=contact)
---- ----
As you can see this looks an awful lot like our "`Show Contact`" functionality. In fact, it is nearly identical except As you can see this looks a lot like our "`Show Contact`" functionality. In fact, it is nearly identical except
for the template that we render: here we render `edit.html` rather than `show.html`. for the template: here we render `edit.html` rather than `show.html`.
While our handler code looked similar to the "`Show Contact`" functionality, the `edit.html` template is going to look While our handler code looked similar to the "`Show Contact`" functionality, the `edit.html` template is going to look
very similar to the template for the "`New Contact`" functionality: we will have a form that submits updated contact very similar to the template for the "`New Contact`" functionality: we will have a form that submits updated contact
@ -697,14 +686,14 @@ Here is the first bit of the form:
<span class="error">{{ contact.errors['email'] }}</span> <span class="error">{{ contact.errors['email'] }}</span>
</p> </p>
---- ----
<1> Issue a `POST` to the `/contacts/{{ contact.id }}/edit` path <1> Issue a `POST` to the `/contacts/{{ contact.id }}/edit` path.
<2> As with the `new.html` page, the input is tied to the contact's email <2> As with the `new.html` page, the input is tied to the contact's email.
This HTML is nearly identical to our `new.html` form, except that this form is going to submit a `POST` to a different This HTML is nearly identical to our `new.html` form, except that this form is going to submit a `POST` to a different
path, based on the id of the contact that we want to update. (It's worth mentioning here that, rather than `POST`, we path, based on the id of the contact that we want to update. (It's worth mentioning here that, rather than `POST`, we
would prefer to use a `PUT` or `PATCH`, but those are not available in plain HTML.) would prefer to use a `PUT` or `PATCH`, but those are not available in plain HTML.)
Following this we have the remainder of our form, again very similar to the `new.html` template, and our submit button Following this we have the remainder of our form, again very similar to the `new.html` template, and our button
to submit the form. to submit the form.
.The Edit Contact Form Body .The Edit Contact Form Body
@ -769,16 +758,16 @@ refactored into a shared template between the edit and create templates, allowin
Repeat Yourself) implementation. Repeat Yourself) implementation.
Note that factoring on the server-side tends to be coarser-grained than on the client-side: you tend to split out common Note that factoring on the server-side tends to be coarser-grained than on the client-side: you tend to split out common
_sections_ rather than create lots of individual components. This has both benefits (it tends to be simple) as well as _sections_ rather than create lots of individual components. This has benefits (it tends to be simple) as well as
drawbacks (it is not nearly as isolated as client-side components) . drawbacks (it is not nearly as isolated as client-side components).
Overall, however, a properly factored server-side hypermedia application can be extremely DRY! Overall, a properly factored server-side hypermedia application can be extremely DRY.
**** ****
==== Handling The Post to `/contacts/<contact_id>` ==== Handling The Post to `/contacts/<contact_id>`
Next we need to handle the HTTP `POST` request that the form in our `edit.html` template submits. We will declare Next we need to handle the HTTP `POST` request that the form in our `edit.html` template submits. We will declare
another route that handles the path as the `GET` above. another route that handles the same path as the `GET` above.
Here is the new handler code: Here is the new handler code:
@ -794,12 +783,12 @@ def contacts_edit_post(contact_id=0):
else: else:
return render_template("edit.html", contact=c) <6> return render_template("edit.html", contact=c) <6>
---- ----
<1> Handle a `POST` to `/contacts/<contact_id>/edit` <1> Handle a `POST` to `/contacts/<contact_id>/edit`.
<2> Look the contact up by id <2> Look the contact up by id.
<3> update the contact with the new information from the form <3> Update the contact with the new information from the form.
<4> Attempt to save it <4> Attempt to save it.
<5> On success, flash a success message & redirect to the detail page <5> On success, flash a success message & redirect to the detail page.
<6> On failure, re-render the edit template, showing any errors <6> On failure, re-render the edit template, showing any errors.
The logic in this handler is very similar to the logic in the handler for adding a new contact. The only real difference The logic in this handler is very similar to the logic in the handler for adding a new contact. The only real difference
is that, rather than creating a new Contact, we look the contact up by id and then call the `update()` method on it with is that, rather than creating a new Contact, we look the contact up by id and then call the `update()` method on it with
@ -825,9 +814,9 @@ def contacts_delete(contact_id=0):
flash("Deleted Contact!") flash("Deleted Contact!")
return redirect("/contacts") <3> return redirect("/contacts") <3>
---- ----
<1> Handle a `POST` the `/contacts/<contact_id>/delete` path <1> Handle a `POST` the `/contacts/<contact_id>/delete` path.
<2> Look up and then invoke the `delete()` method on the contact <2> Look up and then invoke the `delete()` method on the contact.
<3> Flash a success message and redirect to the main list of contacts <3> Flash a success message and redirect to the main list of contacts.
The handler code is very simple since we don't need to do any validation or conditional logic: we simply look up the The handler code is very simple since we don't need to do any validation or conditional logic: we simply look up the
contact the same way we have been doing in our other handlers and invoke the `delete()` method on it, then redirect contact the same way we have been doing in our other handlers and invoke the `delete()` method on it, then redirect
@ -837,12 +826,11 @@ No need for a template in this case, the contact is gone.
=== Contact.app... Implemented! === Contact.app... Implemented!
And, well... believe it or not, that's our entire contact application! The Flask and Jinja2 code should be simple enough And, well... believe it or not, that's our entire contact application!
that you are able to follow along, even if Python isn't your preferred language or Flask isn't your preferred web
application framework. Again, we don't expect you to be a Python or Flask experts (we aren't!) and you shouldn't need
more than a basic understanding of how they work for the remainder of the book.
Now, admittedly, this isn't a large or sophisticated application, but it does demonstrate many of the aspects of If you've struggled with parts of the code so far, don't worry: we don't expect you to be a Python or Flask expert (we aren't!). You just need a basic understanding of how they work to benefit from the remainder of the book.
This is a small and simple application, but it does demonstrate many of the aspects of
traditional, web 1.0 applications: CRUD, the Post/Redirect/Get pattern, working traditional, web 1.0 applications: CRUD, the Post/Redirect/Get pattern, working
with domain logic in a controller, organizing our URLs in a coherent, resource-oriented manner. with domain logic in a controller, organizing our URLs in a coherent, resource-oriented manner.
@ -850,20 +838,20 @@ And, furthermore, this is a deeply _Hypermedia-Driven_ web application. Without
been using REST, HATEOAS and all the other hypermedia concepts we discussed earlier. We would bet that this simple been using REST, HATEOAS and all the other hypermedia concepts we discussed earlier. We would bet that this simple
little contact app of ours is more RESTful than 99% of all JSON APIs ever built! little contact app of ours is more RESTful than 99% of all JSON APIs ever built!
And it was all effortless: just by virtue of using a _hypermedia_, HTML, we naturally fall into the RESTful network Just by virtue of using a _hypermedia_, HTML, we naturally fall into the RESTful network
architecture. architecture.
So that's great. But what's the matter with this little web app? Why not end here and go off to develop the old web 1.0 style So that's great. But what's the matter with this little web app? Why not end here and go off to develop the old web 1.0 style
applications people used to build? applications people used to build?
Well, at some level, nothing is wrong with it. Particularly for an application that is as simple as this one it, the older Well, at some level, nothing is wrong with it. Particularly for an application as simple as this one, the older
way of building web apps might be a perfectly acceptable approach! way of building web apps might be a perfectly acceptable approach.
However, our application does suffer from that "`clunkiness`" that we mentioned earlier when discussing web 1.0 applications: However, our application does suffer from that "`clunkiness`" that we mentioned earlier when discussing web 1.0 applications:
every request replaces the entire screen, introducing a noticeable flicker when navigating between pages. You lose your every request replaces the entire screen, introducing a noticeable flicker when navigating between pages. You lose your
scroll state. You have to click around a bit more than you might in a more sophisticated web application. scroll state. You have to click around a bit more than you might in a more sophisticated web application.
Contact.app, at this point, just doesn't feel like a "`modern`" web application, does it? Contact.app, at this point, just doesn't feel like a "`modern`" web application.
Is it time to reach for a JavaScript framework and JSON APIs to make our contact application more interactive? Is it time to reach for a JavaScript framework and JSON APIs to make our contact application more interactive?