mirror of
https://github.com/bigskysoftware/hypermedia-systems.git
synced 2025-12-05 00:03:55 -05:00
edits, tighten, standardize list format
This commit is contained in:
parent
88ed64b325
commit
86f35164a7
@ -6,7 +6,7 @@
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
and the techniques demonstrated into your preferred programming language and web framework.
|
||||
|
||||
== 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
|
||||
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://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
|
||||
along in the code. Most of the authors hadn't written very much Python before writing this book, and we got the hang of
|
||||
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 much Python before writing this book, and we got the hang of
|
||||
it pretty quickly.
|
||||
|
||||
== 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
|
||||
either, so we will only go into as much detail about it as is necessary to show off hypermedia concepts. However, unlike
|
||||
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.
|
||||
Flask is a very simple but flexible web framework for Python. It might be different than web frameworks
|
||||
you are familiar with, so we will introduce its basic components.
|
||||
|
||||
Thankfully, Flask is simple enough that most web developers shouldn't have a problem following along, so let's go over
|
||||
the core ideas.
|
||||
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 elements.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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>
|
||||
return "Hello World!" <3>
|
||||
----
|
||||
<1> Establishes we are mapping the `/` path as a route
|
||||
<2> The next method is the handler for that route
|
||||
<3> Returns the string "`Hello World!`" to the client
|
||||
<1> Establishes we are mapping the `/` path as a route.
|
||||
<2> The next method is the handler for that route.
|
||||
<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
|
||||
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
|
||||
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
|
||||
hypermedia yet, but, nonetheless, a browser will render it just fine:
|
||||
The handler in this case is dead simple, it just returns a string, "`Hello Flask!`", to the client. This isn't
|
||||
hypermedia yet, but a browser will render it just fine:
|
||||
|
||||
.Hello Flask!
|
||||
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()`
|
||||
|
||||
|
||||
Now the `index()` function simply returns the result of calling 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.
|
||||
Now the `index()` function returns the result of the Flask-supplied `redirect()` function with the path
|
||||
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.
|
||||
|
||||
== 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.
|
||||
|
||||
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
|
||||
* Provide the ability to search the list of contacts
|
||||
* Provide the ability to add a new contact to the list
|
||||
* Provide the ability to view the details of a contact on the list
|
||||
* Provide the ability to edit the details of a contact on the list
|
||||
* Provide the ability to delete a contact from the list
|
||||
* View a list of contacts, including first name, last name, phone and email address
|
||||
* Search the contacts
|
||||
* Add a new contact
|
||||
* View the details of a contact
|
||||
* Edit the details of a contact
|
||||
* 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.
|
||||
|
||||
Note that the source code of Contact.app is available on https://github.com/bigskysoftware/contact-app[GitHub].
|
||||
|
||||
=== 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).
|
||||
|
||||
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:
|
||||
|
||||
* 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>
|
||||
return render_template("index.html", contacts=contacts_set) <4>
|
||||
----
|
||||
<1> Look for the query parameter named `q`, which stands for "`query`"
|
||||
<2> If the parameter exists, call the `Contact.search()` function with it
|
||||
<3> If not, call the `Contact.all()` function
|
||||
<4> pass the result to the `index.html` template to render to the client
|
||||
<1> Look for the query parameter named `q`, which stands for "`query.`"
|
||||
<2> If the parameter exists, call the `Contact.search()` function with it.
|
||||
<3> If not, call the `Contact.all()` function.
|
||||
<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.
|
||||
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
|
||||
for those who are not, let's review what it is. Here is an example URL with a query string in it:
|
||||
Query Strings:: A "`query string`" is part of the URL specification. 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
|
||||
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
|
||||
@ -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.
|
||||
|
||||
Finally, we then render a template, `index.html` that displays the given contacts, passing in the results of whichever
|
||||
of these two functions we ended up calling.
|
||||
Finally, we render a template, `index.html` that displays the given contacts, passing in the results of whichever
|
||||
of these two functions we end up calling.
|
||||
|
||||
.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.
|
||||
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
|
||||
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.
|
||||
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.
|
||||
|
||||
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 focus on `Contact` as a _resource_ and how to effectively provide hypermedia representations
|
||||
We will treat `Contact` as a _resource_, and focus on how to effectively provide hypermedia representations
|
||||
of that resource to clients.
|
||||
****
|
||||
|
||||
==== 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:
|
||||
|
||||
* 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"/>
|
||||
</form>
|
||||
----
|
||||
<1> Set the layout template for this template
|
||||
<2> Delimit the content to be inserted into the layout
|
||||
<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
|
||||
<1> Set the layout template for this template.
|
||||
<2> Delimit the content to be inserted into the layout.
|
||||
<3> Create a search form that will issue an HTTP `GET` to `/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
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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>
|
||||
</table>
|
||||
----
|
||||
<1> Output some headers for our table
|
||||
<2> Iterate over the contacts that were passed in to the template
|
||||
<1> Output some headers for our table.
|
||||
<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.
|
||||
<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
|
||||
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
|
||||
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`)
|
||||
|
||||
* 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>
|
||||
----
|
||||
<1> Link to the page that allows you to create a new contact
|
||||
<2> The closing element of the `content` block
|
||||
<1> Link to the page that allows you to create a new contact.
|
||||
<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
|
||||
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
|
||||
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,
|
||||
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.
|
||||
@ -439,10 +430,10 @@ Here is what our HTML looks like:
|
||||
<span class="error">{{ contact.errors['email'] }}</span> <4>
|
||||
</p>
|
||||
----
|
||||
<1> A form that submits to the `/contacts/new` path, using an HTTP `POST`
|
||||
<2> A label for the first form input
|
||||
<3> the first form input, of type email
|
||||
<4> Any error messages associated with this field
|
||||
<1> A form that submits to the `/contacts/new` path, using an HTTP `POST`.
|
||||
<2> A label for the first form input.
|
||||
<3> The first form input, of type email.
|
||||
<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`.
|
||||
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>
|
||||
----
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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`
|
||||
|
||||
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,
|
||||
but that handles an HTTP `POST` method instead of an HTTP `GET`. We will use the submitted form values to attempt to
|
||||
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
|
||||
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.
|
||||
@ -522,13 +512,13 @@ def contacts_new():
|
||||
else:
|
||||
return render_template("new.html", contact=c) <4>
|
||||
----
|
||||
<1> We construct a new contact object with the values from the form
|
||||
<2> We try to save it
|
||||
<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
|
||||
<1> We construct a new contact object with the values from the form.
|
||||
<2> We try to save it.
|
||||
<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.
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
`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.)
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
****
|
||||
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
|
||||
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.
|
||||
@ -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
|
||||
our handler logic will get, even when we look at adding more sophisticated htmx-driven behaviors. Simplicity is a great
|
||||
selling point for hypermedia!
|
||||
our handler logic will get, even when we look at adding more sophisticated htmx-driven behaviors.
|
||||
|
||||
=== 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
|
||||
`/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
|
||||
resources are organized in a coherent manner:
|
||||
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.
|
||||
|
||||
* 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 wish to view a specific contact (with, say, an id of `42), you issue a `GET` to `/contacts/42`
|
||||
* If you wish to view all contacts, you issue a `GET` to `/contacts`.
|
||||
* 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`.
|
||||
|
||||
.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`?`"
|
||||
|
||||
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
|
||||
getting too worked up about the smaller details of your URL design.
|
||||
important to understand the overarching idea of _resources_ and _hypermedia representations_, rather than
|
||||
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,
|
||||
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>
|
||||
return render_template("show.html", contact=contact) <4>
|
||||
----
|
||||
<1> Map the path, with a path variable named `contact_id`
|
||||
<2> The handler takes the value of this path parameter
|
||||
<3> Look up the corresponding contact
|
||||
<4> Render the `show.html` template
|
||||
<1> Map the path, with a path variable named `contact_id`.
|
||||
<2> The handler takes the value of this path parameter.
|
||||
<3> Look up the corresponding contact.
|
||||
<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
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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>
|
||||
----
|
||||
|
||||
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.
|
||||
|
||||
=== Editing And Deleting A Contact
|
||||
@ -675,8 +664,8 @@ def contacts_edit_get(contact_id=0):
|
||||
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
|
||||
for the template that we render: here we render `edit.html` rather than `show.html`.
|
||||
As you can see this looks a lot like our "`Show Contact`" functionality. In fact, it is nearly identical except
|
||||
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
|
||||
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>
|
||||
</p>
|
||||
----
|
||||
<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
|
||||
<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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
.The Edit Contact Form Body
|
||||
@ -769,16 +758,16 @@ refactored into a shared template between the edit and create templates, allowin
|
||||
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
|
||||
_sections_ rather than create lots of individual components. This has both benefits (it tends to be simple) as well as
|
||||
drawbacks (it is not nearly as isolated as client-side components) .
|
||||
_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).
|
||||
|
||||
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>`
|
||||
|
||||
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:
|
||||
|
||||
@ -794,12 +783,12 @@ def contacts_edit_post(contact_id=0):
|
||||
else:
|
||||
return render_template("edit.html", contact=c) <6>
|
||||
----
|
||||
<1> Handle a `POST` to `/contacts/<contact_id>/edit`
|
||||
<2> Look the contact up by id
|
||||
<3> update the contact with the new information from the form
|
||||
<4> Attempt to save it
|
||||
<5> On success, flash a success message & redirect to the detail page
|
||||
<6> On failure, re-render the edit template, showing any errors
|
||||
<1> Handle a `POST` to `/contacts/<contact_id>/edit`.
|
||||
<2> Look the contact up by id.
|
||||
<3> Update the contact with the new information from the form.
|
||||
<4> Attempt to save it.
|
||||
<5> On success, flash a success message & redirect to the detail page.
|
||||
<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
|
||||
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!")
|
||||
return redirect("/contacts") <3>
|
||||
----
|
||||
<1> Handle a `POST` the `/contacts/<contact_id>/delete` path
|
||||
<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
|
||||
<1> Handle a `POST` the `/contacts/<contact_id>/delete` path.
|
||||
<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.
|
||||
|
||||
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
|
||||
@ -837,12 +826,11 @@ No need for a template in this case, the contact is gone.
|
||||
|
||||
=== Contact.app... Implemented!
|
||||
|
||||
And, well... believe it or not, that's our entire contact application! The Flask and Jinja2 code should be simple enough
|
||||
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.
|
||||
And, well... believe it or not, that's our entire contact application!
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
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?
|
||||
|
||||
Well, at some level, nothing is wrong with it. Particularly for an application that is as simple as this one it, the older
|
||||
way of building web apps might be a perfectly acceptable approach!
|
||||
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.
|
||||
|
||||
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
|
||||
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?
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user