Real Python: Python REST APIs With Flask, Connexion, and SQLAlchemy – Part 3

In Part 2 of this series, you added the ability to save changes made through the REST API to a database using SQLAlchemy and learned how to serialize that data for the REST API using Marshmallow. Connecting the REST API to a database so that the application can make changes to existing data and create new data is great and makes the application much more useful and robust.

That’s only part of the power a database offers, however. An even more powerful feature is the R part of RDBMS systems: relationships. In a database, a relationship is the ability to connect two or more tables together in a meaningful way. In this article, you’ll learn how to implement relationships and turn your Person database into a mini-blogging web application.

In this article, you’ll learn:

  • Why more than one table in a database is useful and important
  • How tables are related to each other
  • How SQLAlchemy can help you manage relationships
  • How relationships help you build a mini-blogging application

Who This Article Is For

Part 1 of this series guided you through building a REST API, and Part 2 showed you how to connect that REST API to a database.

This article expands your programming tool belt further. You’ll learn how to create hierarchical data structures represented as one-to-many relationships by SQLAlchemy. In addition, you’ll extend the REST API you’ve already built to provide CRUD (Create, Read, Update, and Delete) support for the elements in this hierarchical structure.

The web application presented in Part 2 will have its HTML and JavaScript files modified in major ways to create a more fully functional mini-blogging application. You can review the final version of the code from Part 2 in the GitHub repository for that article.

Hang on as you get started creating relationships and your mini-blogging application!

Additional Dependencies

There are no new Python dependencies beyond what was required for the Part 2 article. However, you will be using two new JavaScript modules in the web application to makes things easier and more consistent. The two modules are the following:

  1. Handlebars.js is a templating engine for JavaScript, much like Jinja2 for Flask.
  2. Moment.js is a datetime parsing and formatting module that makes displaying UTC timestamps easier.

You don’t have to download either of these, as the web application will get them directly from the Cloudflare CDN (Content Delivery Network), as you’re already doing for the jQuery module.

People Data Extended for Blogging

In Part 2, the People data existed as a dictionary in the build_database.py Python code. This is what you used to populate the database with some initial data. You’re going to modify the People data structure to give each person a list of notes associated with them. The new People data structure will look like this:

# Data to initialize database with PEOPLE = [     {         "fname": "Doug",         "lname": "Farrell",         "notes": [             ("Cool, a mini-blogging application!", "2019-01-06 22:17:54"),             ("This could be useful", "2019-01-08 22:17:54"),             ("Well, sort of useful", "2019-03-06 22:17:54"),         ],     },     {         "fname": "Kent",         "lname": "Brockman",         "notes": [             (                 "I'm going to make really profound observations",                 "2019-01-07 22:17:54",             ),             (                 "Maybe they'll be more obvious than I thought",                 "2019-02-06 22:17:54",             ),         ],     },     {         "fname": "Bunny",         "lname": "Easter",         "notes": [             ("Has anyone seen my Easter eggs?", "2019-01-07 22:47:54"),             ("I'm really late delivering these!", "2019-04-06 22:17:54"),         ],     }, ] 

Each person in the People dictionary now includes a key called notes, which is associated with a list containing tuples of data. Each tuple in the notes list represents a single note containing the content and a timestamp. The timestamps are initialized (rather than dynamically created) to demonstrate ordering later on in the REST API.

Each single person is associated with multiple notes, and each single note is associated with only one person. This hierarchy of data is known as a one-to-many relationship, where a single parent object is related to many child objects. You’ll see how this one-to-many relationship is managed in the database with SQLAlchemy.

Brute Force Approach

The database you built stored the data in a table, and a table is a two-dimensional array of rows and columns. Can the People dictionary above be represented in a single table of rows and columns? It can be, in the following way, in your person database table. Unfortunately to include all of the actual data in the example creates a scroll bar for the table, as you’ll see below:

person_id lname fname timestamp content note_timestamp
1 Farrell Doug 2018-08-08 21:16:01 Cool, a mini-blogging application! 2019-01-06 22:17:54
2 Farrell Doug 2018-08-08 21:16:01 This could be useful 2019-01-08 22:17:54
3 Farrell Doug 2018-08-08 21:16:01 Well, sort of useful 2019-03-06 22:17:54
4 Brockman Kent 2018-08-08 21:16:01 I’m going to make really profound observations 2019-01-07 22:17:54
5 Brockman Kent 2018-08-08 21:16:01 Maybe they’ll be more obvious than I thought 2019-02-06 22:17:54
6 Easter Bunny 2018-08-08 21:16:01 Has anyone seen my Easter eggs? 2019-01-07 22:47:54
7 Easter Bunny 2018-08-08 21:16:01 I’m really late delivering these! 2019-04-06 22:17:54

The above table would actually work. All the data is represented, and a single person is associated with a collection of different notes.

Advantages

Conceptually, the above table structure has the advantage of being relatively simple to understand. You could even make the case that the data could be persisted to a flat file instead of a database.

Because of the two-dimensional table structure, you could store and use this data in a spreadsheet. Spreadsheets have been pressed into service as data storage quite a bit.

Disadvantages

While the above table structure would work, it has some real disadvantages.

In order to represent the collection of notes, all the data for each person is repeated for every unique note, the person data is therefore redundant. This isn’t such a big deal for your person data as there aren’t that many columns. But imagine if a person had many more columns. Even with large disk drives, this can get to be a storage concern if you’re dealing with millions of rows of data.

Having redundant data like this can lead to maintenance issues as time goes by. For example, what if the Easter Bunny decided a change of name was a good idea. In order to do this, every record containing the Easter Bunny’s name would have to be updated in order to keep the data consistent. This kind of work against the database can lead to data inconsistency, particularly if the work is done by a person running a SQL query by hand.

Naming columns becomes awkward. In the table above, there is a timestamp column used to track the creation and update time of a person in the table. You also want to have similar functionality for the creation and update time for a note, but because timestamp is already used, a contrived name of note_timestamp is used.

What if you wanted to add additional one-to-many relationships to the person table? For example, to include a person’s children or phone numbers. Each person could have multiple children and multiple phone numbers. This could be done relatively easily to the Python People dictionary above by adding children and phone_numbers keys with new lists containing the data.

However, representing those new one-to-many relationships in your person database table above becomes significantly more difficult. Every new one-to-many relationship increases the number of rows necessary to represent it for every single entry in the child data dramatically. In addition, the problems associated with data redundancy get bigger and more difficult to handle.

Lastly, the data you’d get back from the above table structure wouldn’t be very Pythonic: it would be just a big list of lists. SQLAlchemy wouldn’t be able to help you very much because the relationship isn’t there.

Relational Database Approach

Based on what you’ve seen above, it becomes clear that trying to represent even a moderately complex dataset in a single table becomes unmanageable pretty quickly. Given that, what alternative does a database offer? This is where the R part of RDBMS databases comes into play. Representing relationships removes the disadvantages outlined above.

Instead of trying to represent hierarchical data in a single table, the data is broken up into multiple tables, with a mechanism to relate them to one another. The tables are broken along collection lines, so for your People dictionary above, this means there will be a table representing people and another representing notes. This brings back your original person table, which looks like this:

person_id lname fname timestamp
1 Farrell Doug 2018-08-08 21:16:01.888444
2 Brockman Kent 2018-08-08 21:16:01.889060
3 Easter Bunny 2018-08-08 21:16:01.886834

To represent the new note information, you’ll create a new table called note. (Remember our singular table naming convention.) The table looks like this:

note_id person_id content timestamp
1 1 Cool, a mini-blogging application! 2019-01-06 22:17:54
2 1 This could be useful 2019-01-08 22:17:54
3 1 Well, sort of useful 2019-03-06 22:17:54
4 2 I’m going to make really profound observations 2019-01-07 22:17:54
5 2 Maybe they’ll be more obvious than I thought 2019-02-06 22:17:54
6 3 Has anyone seen my Easter eggs? 2019-01-07 22:47:54
7 3 I’m really late delivering these! 2019-04-06 22:17:54

Notice that, like the person table, the note table has a unique identifier called note_id, which is the primary key for the note table. One thing that isn’t obvious is the inclusion of the person_id value in the table. What is that used for? This is what creates the relationship to the person table. Whereas note_id is the primary key for the table, person_id is what’s known as a foreign key.

The foreign key gives each entry in the note table the primary key of the person record it’s associated with. Using this, SQLAlchemy can gather all the notes associated with each person by connecting the person.person_id primary key to the note.person_id foreign key, creating a relationship.

Advantages

By breaking the data set into two tables, and introducing the concept of a foreign key, you’ve made the data a little more complex to think about, you have resolved the disadvantages of a single table representation. SQLAlchemy will help you encode the increased complexity fairly easily.

The data is no longer redundant in the database. There is only one person entry for each person you want to store in the database. This solves the storage concern immediately and dramatically simplifies the maintenance concerns.

If the Easter Bunny still wanted to change names, then you’d only have to change a single row in the person table, and anything else related to that row (like the note table) would immediately take advantage of the change.

Column naming is more consistent and meaningful. Because person and note data exist in separate tables, the creation and update timestamp can be named consistently in both tables, as there is no conflict for names across tables.

In addition, you’d no longer have to create permutations of each row for new one-to-many relationships you might want to represent. Take our children and phone_numbers example from earlier. Implementing this would require child and phone_number tables. Each table would contain a foreign key of person_id relating it back to the person table.

Using SQLAlchemy, the data you’d get back from the above tables would be more immediately useful, as what you’d get is an object for each person row. That object has named attributes equivalent to the columns in the table. One of those attributes is a Python list containing the related note objects.

Disadvantages

Where the brute force approach was simpler to understand, the concept of foreign keys and relationships make thinking about the data somewhat more abstract. This abstraction needs to be thought about for every relationship you establish between tables.

Making use of relationships means committing to using a database system. This is another tool to install, learn, and maintain above and beyond the application that actually uses the data.

SQLAlchemy Models

To use the two tables above, and the relationship between them, you’ll need to create SQLAlchemy models that are aware of both tables and the relationship between them. Here’s the SQLAlchemy Person model from Part 2, updated to include a relationship to a collection of notes:

 1 class Person(db.Model):  2     __tablename__ = 'person'  3     person_id = db.Column(db.Integer, primary_key=True)  4     lname = db.Column(db.String(32))  5     fname = db.Column(db.String(32))  6     timestamp = db.Column(  7         db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow  8     )  9     notes = db.relationship( 10         'Note', 11         backref='person', 12         cascade='all, delete, delete-orphan', 13         single_parent=True, 14         order_by='desc(Note.timestamp)' 15     ) 

Lines 1 to 8 of the above Python class look exactly like what you created before in Part 2. Lines 9 to 16 create a new attribute in the Person class called notes. This new notes attributes is defined in the following lines of code:

  • Line 9: Like the other attributes of the class, this line creates a new attribute called notes and sets it equal to an instance of an object called db.relationship. This object creates the relationship you’re adding to the Person class and is created with all of the parameters defined in the lines that follow.

  • Line 10: The string parameter 'Note' defines the SQLAlchemy class that the Person class will be related to. The Note class isn’t defined yet, which is why it’s a string here. This is a forward reference and helps handle problems that the order of definitions could cause when something is needed that isn’t defined until later in the code. The 'Note' string allows the Person class to find the Note class at runtime, which is after both Person and Note have been defined.

  • Line 11: The backref='person' parameter is trickier. It creates what’s known as a backwards reference in Note objects. Each instance of a Note object will contain an attribute called person. The person attribute references the parent object that a particular Note instance is associated with. Having a reference to the parent object (person in this case) in the child can be very useful if your code iterates over notes and has to include information about the parent. This happens surprisingly often in display rendering code.

  • Line 12: The cascade='all, delete, delete-orphan' parameter determines how to treat note object instances when changes are made to the parent Person instance. For example, when a Person object is deleted, SQLAlchemy will create the SQL necessary to delete the Person from the database. Additionally, this parameter tells it to also delete all the Note instances associated with it. You can read more about these options in the SQLAlchemy documentation.

  • Line 13: The single_parent=True parameter is required if delete-orphan is part of the previous cascade parameter. This tells SQLAlchemy not to allow orphaned Note instances (a Note without a parent Person object) to exist because each Note has a single parent.

  • Line 14: The order_by='desc(Note.timestamp)' parameter tells SQLAlchemy how to sort the Note instances associated with a Person. When a Person object is retrieved, by default the notes attribute list will contain Note objects in an unknown order. The SQLAlchemy desc(...) function will sort the notes in descending order from newest to oldest. If this line was instead order_by='Note.timestamp', SQLAlchemy would default to using the asc(...) function, and sort the notes in ascending order, oldest to newest.

Now that your Person model has the new notes attribute, and this represents the one-to-many relationship to Note objects, you’ll need to define a SQLAlchemy model for a Note:

 1 class Note(db.Model):  2     __tablename__ = 'note'  3     note_id = db.Column(db.Integer, primary_key=True)  4     person_id = db.Column(db.Integer, db.ForeignKey('person.person_id'))  5     content = db.Column(db.String, nullable=False)  6     timestamp = db.Column(  7         db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow  8     ) 

The Note class defines the attributes making up a note as seen in our sample note database table from above. The attributes are defined here:

  • Line 1 creates the Note class, inheriting from db.Model, exactly as you did before when creating the Person class.

  • Line 2 tells the class what database table to use to store Note objects.

  • Line 3 creates the note_id attribute, defining it as an integer value, and as the primary key for the Note object.

  • Line 4 creates the person_id attribute, and defines it as the foreign key, relating the Note class to the Person class using the person.person_id primary key. This, and the Person.notes attribute, are how SQLAlchemy knows what to do when interacting with Person and Note objects.

  • Line 5 creates the content attribute, which contains the actual text of the note. The nullable=False parameter indicates that it’s okay to create new notes that have no content.

  • Line 6 creates the timestamp attribute, and exactly like the Person class, this contains the creation or update time for any particular Note instance.

Initialize the Database

Now that you’ve updated the Person and created the Note models, you’ll use them to rebuild the testing database people.db. You’ll do this by updating the build_database.py code from Part 2. Here’s what the code will look like:

 1 import os  2 from datetime import datetime  3 from config import db  4 from models import Person, Note  5   6 # Data to initialize database with  7 PEOPLE = [  8     {  9         "fname": "Doug", 10         "lname": "Farrell", 11         "notes": [ 12             ("Cool, a mini-blogging application!", "2019-01-06 22:17:54"), 13             ("This could be useful", "2019-01-08 22:17:54"), 14             ("Well, sort of useful", "2019-03-06 22:17:54"), 15         ], 16     }, 17     { 18         "fname": "Kent", 19         "lname": "Brockman", 20         "notes": [ 21             ( 22                 "I'm going to make really profound observations", 23                 "2019-01-07 22:17:54", 24             ), 25             ( 26                 "Maybe they'll be more obvious than I thought", 27                 "2019-02-06 22:17:54", 28             ), 29         ], 30     }, 31     { 32         "fname": "Bunny", 33         "lname": "Easter", 34         "notes": [ 35             ("Has anyone seen my Easter eggs?", "2019-01-07 22:47:54"), 36             ("I'm really late delivering these!", "2019-04-06 22:17:54"), 37         ], 38     }, 39 ] 40  41 # Delete database file if it exists currently 42 if os.path.exists("people.db"): 43     os.remove("people.db") 44  45 # Create the database 46 db.create_all() 47  48 # Iterate over the PEOPLE structure and populate the database 49 for person in PEOPLE: 50     p = Person(lname=person.get("lname"), fname=person.get("fname")) 51  52     # Add the notes for the person 53     for note in person.get("notes"): 54         content, timestamp = note 55         p.notes.append( 56             Note( 57                 content=content, 58                 timestamp=datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S"), 59             ) 60         ) 61     db.session.add(p) 62  63 db.session.commit() 

The code above came from Part 2, with a few changes to create the one-to-many relationship between Person and Note. Here are the updated or new lines added to the code:

  • Line 4 has been updated to import the Note class defined previously.

  • Lines 7 to 39 contain the updated PEOPLE dictionary containing our person data, along with the list of notes associated with each person. This data will be inserted into the database.

  • Lines 49 to 61 iterate over the PEOPLE dictionary, getting each person in turn and using it to create a Person object.

  • Line 53 iterates over the person.notes list, getting each note in turn.

  • Line 54 unpacks the content and timestamp from each note tuple.

  • Line 55 to 60 creates a Note object and appends it to the person notes collection using p.notes.append().

  • Line 61 adds the Person object p to the database session.

  • Line 63 commits all of the activity in the session to the database. It’s at this point that all of the data is written to the person and note tables in the people.db database file.

You can see that working with the notes collection in the Person object instance p is just like working with any other list in Python. SQLAlchemy takes care of the underlying one-to-many relationship information when the db.session.commit() call is made.

For example, just like a Person instance has its primary key field person_id initialized by SQLAlchemy when it’s committed to the database, instances of Note will have their primary key fields initialized. In addition, the Note foreign key person_id will also be initialized with the primary key value of the Person instance it’s associated with.

Here’s an example instance of a Person object before the db.session.commit() in a kind of pseudocode:

Person (     person_id = None     lname = 'Farrell'     fname = 'Doug'     timestamp = None     notes = [         Note (             note_id = None             person_id = None             content = 'Cool, a mini-blogging application!'             timestamp = '2019-01-06 22:17:54'         ),         Note (             note_id = None             person_id = None             content = 'This could be useful'             timestamp = '2019-01-08 22:17:54'         ),         Note (             note_id = None             person_id = None             content = 'Well, sort of useful'             timestamp = '2019-03-06 22:17:54'         )     ] ) 

Here’s the example Person object after the db.session.commit():

Person (     person_id = 1     lname = 'Farrell'     fname = 'Doug'     timestamp = '2019-02-02 21:27:10.336'     notes = [         Note (             note_id = 1             person_id = 1             content = 'Cool, a mini-blogging application!'             timestamp = '2019-01-06 22:17:54'         ),         Note (             note_id = 2             person_id = 1             content = 'This could be useful'             timestamp = '2019-01-08 22:17:54'         ),         Note (             note_id = 3             person_id = 1             content = 'Well, sort of useful'             timestamp = '2019-03-06 22:17:54'         )     ] ) 

The important difference between the two is that the primary key of the Person and Note objects has been initialized. The database engine took care of this as the objects were created because of the auto-incrementing feature of primary keys discussed in Part 2.

Additionally, the person_id foreign key in all the Note instances has been initialized to reference its parent. This happens because of the order in which the Person and Note objects are created in the database.

SQLAlchemy is aware of the relationship between Person and Note objects. When a Person object is committed to the person database table, SQLAlchemy gets the person_id primary key value. That value is used to initialize the foreign key value of person_id in a Note object before it’s committed to the database.

SQLAlchemy takes care of this database housekeeping work because of the information you passed when the Person.notes attribute was initialized with the db.relationship(...) object.

In addition, the Person.timestamp attribute has been initialized with the current timestamp.

Running the build_database.py program from the command line (in the virtual environment will re-create the database with the new additions, getting it ready for use with the web application. This command line will rebuild the database:

$   python build_database.py 

The build_database.py utility program doesn’t output any messages if it runs successfully. If it throws an exception, then an error will be printed on the screen.

Update REST API

You’ve updated the SQLAlchemy models and used them to update the people.db database. Now it’s time to update the REST API to provide access to the new notes information. Here’s the REST API you built in Part 2:

Action HTTP Verb URL Path Description
Create POST /api/people URL to create a new person
Read GET /api/people URL to read a collection of people
Read GET /api/people/{person_id} URL to read a single person by person_id
Update PUT /api/people/{person_id} URL to update an existing person by person_id
Delete DELETE /api/people/{person_id} URL to delete an existing person by person_id

The REST API above provides HTTP URL paths to collections of things, and to the things themselves. You can get a list of people or interact with a single person from that list of people. This path style refines what’s returned in a left-to-right manner, getting more granular as you go.

You’ll continue this left-to-right pattern to get more granular and access the notes collections. Here’s the extended REST API you’ll create in order to provide notes to the mini-blog web application:

Action HTTP Verb URL Path Description
Create POST /api/people/{person_id}/notes URL to create a new note
Read GET /api/people/{person_id}/notes/{note_id} URL to read a single person’s single note
Update PUT api/people/{person_id}/notes/{note_id} URL to update a single person’s single note
Delete DELETE api/people/{person_id}/notes/{note_id} URL to delete a single person’s single note
Read GET /api/notes URL to get all notes for all people sorted by note.timestamp

There are two variations in the notes part of the REST API compared to the convention used in the people section:

  1. There is no URL defined to get all the notes associated with a person, only a URL to get a single note. This would have made the REST API complete in a way, but the web application you’ll create later doesn’t need this functionality. Therefore, it’s been left out.

  2. There is the inclusion of the last URL /api/notes. This is a convenience method created for the web application. It will be used in the mini-blog on the home page to show all the notes in the system. There isn’t a way to get this information readily using the REST API pathing style as designed, so this shortcut has been added.

As in Part 2, the REST API is configured in the swagger.yml file.

Note:

The idea of designing a REST API with a path that gets more and more granular as you move from left to right is very useful. Thinking this way can help clarify the relationships between different parts of a database. Just be aware that there are realistic limits to how far down a hierarchical structure this kind of design should be taken.

For example, what if the Note object had a collection of its own, something like comments on the notes. Using the current design ideas, this would lead to a URL that went something like this: /api/people/{person_id}/notes/{note_id}/comments/{comment_id}

There is no practical limit to this kind of design, but there is one for usefulness. In actual use in real applications, a long, multilevel URL like that one is hardly ever needed. A more common pattern is to get a list of intervening objects (like notes) and then use a separate API entry point to get a single comment for an application use case.

Implement the API

With the updated REST API defined in the swagger.yml file, you’ll need to update the implementation provided by the Python modules. This means updating existing module files, like models.py and people.py, and creating a new module file called notes.py to implement support for Notes in the extended REST API.

Update Response JSON

The purpose of the REST API is to get useful JSON data out of the database. Now that you’ve updated the SQLAlchemy Person and created the Note models, you’ll need to update the Marshmallow schema models as well. As you may recall from Part 2, Marshmallow is the module that translates the SQLAlchemy objects into Python objects suitable for creating JSON strings.

The updated and newly created Marshmallow schemas are in the models.py module, which are explained below, and look like this:

 1 class PersonSchema(ma.ModelSchema):  2     class Meta:  3         model = Person  4         sqla_session = db.session  5     notes = fields.Nested('PersonNoteSchema', default=[], many=True)  6   7 class PersonNoteSchema(ma.ModelSchema):  8     """  9     This class exists to get around a recursion issue 10     """ 11     note_id = fields.Int() 12     person_id = fields.Int() 13     content = fields.Str() 14     timestamp = fields.Str() 15  16 class NoteSchema(ma.ModelSchema): 17     class Meta: 18         model = Note 19         sqla_session = db.session 20     person = fields.Nested('NotePersonSchema', default=None) 21  22 class NotePersonSchema(ma.ModelSchema): 23     """ 24     This class exists to get around a recursion issue 25     """ 26     person_id = fields.Int() 27     lname = fields.Str() 28     fname = fields.Str() 29     timestamp = fields.Str() 

There are some interesting things going on in the above definitions. The PersonSchema class has one new entry: the notes attribute defined in line 5. This defines it as a nested relationship to the PersonNoteSchema. It will default to an empty list if nothing is present in the SQLAlchemy notes relationship. The many=True parameter indicates that this is a one-to-many relationship, so Marshmallow will serialize all the related notes.

The PersonNoteSchema class defines what a Note object looks like as Marshmallow serializes the notes list. The NoteSchema defines what a SQLAlchemy Note object looks like in terms of Marshmallow. Notice that it has a person attribute. This attribute comes from the SQLAlchemy db.relationship(...) definition parameter backref='person'. The person Marshmallow definition is nested, but because it doesn’t have the many=True parameter, there is only a single person connected.

The NotePersonSchema class defines what is nested in the NoteSchema.person attribute.

Note:

You might be wondering why the PersonSchema class has its own unique PersonNoteSchema class to define the notes collection attribute. By the same token, the NoteSchema class has its own unique NotePersonSchema class to define the person attribute. You may be wondering whether the PersonSchema class could be defined this way:

class PersonSchema(ma.ModelSchema):     class Meta:         model = Person         sqla_session = db.session     notes = fields.Nested('NoteSchema', default=[], many=True) 

Additionally, couldn’t the NoteSchema class be defined using the PersonSchema to define the person attribute? A class definition like this would each refer to the other, and this causes a recursion error in Marshmallow as it will cycle from PersonSchema to NoteSchema until it runs out of stack space. Using the unique schema references breaks the recursion and allows this kind of nesting to work.

People

Now that you’ve got the schemas in place to work with the one-to-many relationship between Person and Note, you need to update the person.py and create the note.py modules in order to implement a working REST API.

The people.py module needs two changes. The first is to import the Note class, along with the Person class at the top of the module. Then only read_one(person_id) needs to change in order to handle the relationship. That function will look like this:

 1 def read_one(person_id):  2     """  3     This function responds to a request for /api/people/{person_id}  4     with one matching person from people  5   6     :param person_id:   Id of person to find  7     :return:            person matching id  8     """  9     # Build the initial query 10     person = ( 11         Person.query.filter(Person.person_id == person_id) 12         .outerjoin(Note) 13         .one_or_none() 14     ) 15  16     # Did we find a person? 17     if person is not None: 18  19         # Serialize the data for the response 20         person_schema = PersonSchema() 21         data = person_schema.dump(person).data 22         return data 23  24     # Otherwise, nope, didn't find that person 25     else: 26         abort(404, f"Person not found for Id: {person_id}") 

The only difference is line 12: .outerjoin(Note). An outer join (left outer join in SQL terms) is necessary for the case where a user of the application has created a new person object, which has no notes related to it. The outer join ensures that the SQL query will return a person object, even if there are no note rows to join with.

At the start of this article, you saw how person and note data could be represented in a single, flat table, and all of the disadvantages of that approach. You also saw the advantages of breaking that data up into two tables, person and note, with a relationship between them.

Until now, we’ve been working with the data as two distinct, but related, items in the database. But now that you’re actually going to use the data, what we essentially want is for the data to be joined back together. This is what a database join does. It combines data from two tables together using the primary key to foreign key relationship.

A join is kind of a boolean and operation because it only returns data if there is data in both tables to combine. If, for example, a person row exists but has no related note row, then there is nothing to join, so nothing is returned. This isn’t what you want for read_one(person_id).

This is where the outer join comes in handy. It’s a kind of boolean or operation. It returns person data even if there is no associated note data to combine with. This is the behavior you want for read_one(person_id) to handle the case of a newly created Person object that has no notes yet.

You can see the complete people.py in the article repository.

Notes

You’ll create a notes.py module to implement all the Python code associated with the new note related REST API definitions. In many ways, it works like the people.py module, except it must handle both a person_id and a note_id as defined in the swagger.yml configuration file. As an example, here is read_one(person_id, note_id):

 1 def read_one(person_id, note_id):  2     """  3     This function responds to a request for  4     /api/people/{person_id}/notes/{note_id}  5     with one matching note for the associated person  6   7     :param person_id:       Id of person the note is related to  8     :param note_id:         Id of the note  9     :return:                json string of note contents 10     """ 11     # Query the database for the note 12     note = ( 13         Note.query.join(Person, Person.person_id == Note.person_id) 14         .filter(Person.person_id == person_id) 15         .filter(Note.note_id == note_id) 16         .one_or_none() 17     ) 18  19     # Was a note found? 20     if note is not None: 21         note_schema = NoteSchema() 22         data = note_schema.dump(note).data 23         return data 24  25     # Otherwise, nope, didn't find that note 26     else: 27         abort(404, f"Note not found for Id: {note_id}") 

The interesting parts of the above code are lines 12 to 17:

  • Line 13 begins a query against the Note SQLAlchemy objects and joins to the related Person SQLAlchemy object comparing person_id from both Person and Note.
  • Line 14 filters the result down to the Note objects that has a Person.person_id equal to the passed in person_id parameter.
  • Line 15 filters the result further to the Note object that has a Note.note_id equal to the passed in note_id parameter.
  • Line 16 returns the Note object if found, or None if nothing matching the parameters is found.

You can check out the complete notes.py.

Updated Swagger UI

The Swagger UI has been updated by the action of updating the swagger.yml file and creating the URL endpoint implementations. Below is a screenshot of the updated UI showing the Notes section with the GET /api/people/{person_id}/notes/{note_id} expanded:

Swagger UI with notes part 3

Mini-Blogging Web Application

The web application has been substantially changed to show its new purpose as a mini-blogging application. It has three pages:

  1. The home page (localhost:5000/), which shows all of the blog messages (notes) sorted from newest to oldest

  2. The people page (localhost:5000/people), which shows all the people in the system, sorted by last name, and also allows the user to create a new person and update or delete an existing one

  3. The notes page (localhost:5000/people/{person_id}/notes), which shows all the notes associated with a person, sorted from newest to oldest, and also allows the user to create a new note and update or delete an existing one

There are two buttons on every page of the application:

  1. The Home button will navigate to the home screen.
  2. The People button navigates to the /people screen, showing all people in the database.

These two buttons are present on every screen in the application as a way to get back to a starting point.

Home Page

Below is a screenshot of the home page showing the initialized database contents:

Flask Connexion Rest Home Page Part 3

The functionality of this page works like this:

  • Double-clicking on a person’s name will take the user to the /people/{person_id} page, with the editor section filled in with the person’s first and last names and the update and reset buttons enabled.

  • Double-clicking on a person’s note will take the user to the /people/{person_id}/notes/{note_id} page, with the editor section filled in with the note’s contents and the Update and Reset buttons enabled.

People Page

Below is a screenshot of the people page showing the people in the initialized database:

Flask Connexion Rest People Page Part 3

The functionality of this page works like this:

  • Single-clicking on a person’s name will populate the editor section of the page with the person’s first and last name, disabling the Create button, and enabling the Update and Delete buttons.

  • Double clicking on a person’s name will navigate to the notes pages for that person.

The functionality of the editor works like this:

  • If the first and last name fields are empty, the Create and Reset buttons are enabled. Entering a new name in the fields and clicking Create will create a new person and update the database and re-render the table below the editor. Clicking Reset will clear the editor fields.

  • If the first and last name fields have data, the user navigated here by double-clicking the person’s name from the home screen. In this case, the Update, Delete, and Reset buttons are enabled. Changing the first or last name and clicking Update will update the database and re-render the table below the editor. Clicking Delete will remove the person from the database and re-render the table.

Notes Page

Below is a screenshot of the notes page showing the notes for a person in the initialized database:

Flask Connexion Rest Notes Page Part 3

The functionality of this page works like this:

  • Single-clicking on a note will populate the editor section of the page with the notes content, disabling the Create button, and enabling the Update and Delete buttons.

  • All other functionality of this page is in the editor section.

The functionality of the editor works like this:

  • If the note content field is empty, then the Create and Reset buttons are enabled. Entering a new note in the field and clicking Create will create a new note and update the database and re-render the table below the editor. Clicking Reset will clear the editor fields.

  • If the note field has data, the user navigated here by double-clicking the person’s note from the home screen. In this case, the Update, Delete, and Reset buttons are enabled. Changing the note and clicking Update will update the database and re-render the table below the editor. Clicking Delete will remove the note from the database and re-render the table.

Web Application

This article is primarily focused on how to use SQLAlchemy to create relationships in the database, and how to extend the REST API to take advantage of those relationships. As such, the code for the web application didn’t get much attention. When you look at the web application code, keep an eye out for the following features:

  • Each page of the application is a fully formed single page web application.

  • Each page of the application is driven by JavaScript following an MVC (Model/View/Controller) style of responsibility delegation.

  • The HTML that creates the pages takes advantage of the Jinja2 inheritance functionality.

  • The hardcoded JavaScript table creation has been replaced by using the Handlebars.js templating engine.

  • The timestamp formating in all of the tables is provided by Moment.js.

You can find the following code in the repository for this article:

All of the example code for this article is available in the GitHub repository for this article. This contains all of the code related to this article, including all of the web application code.

Conclusion

Congratulations are in order for what you’ve learned in this article! Knowing how to build and use database relationships gives you a powerful tool to solve many difficult problems. There are other relationship besides the one-to-many example from this article. Other common ones are one-to-one, many-to-many, and many-to-one. All of them have a place in your toolbelt, and SQLAlchemy can help you tackle them all!

For more information about databases, you can check out these tutorials. You can also set up Flask to use SQLAlchemy. You can check out Model-View-Controller (MVC) more information about the pattern used in the web application JavaScript code.

In Part 4 of this series, you’ll focus on the HTML, CSS, and JavaScript files used to create the web application.


[ Improve Your Python With 🐍 Python Tricks πŸ’Œ – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]

Planet Python

Real Python: Get Started With Django Part 1: Build a Portfolio App

Django is a fully featured Python web framework that can be used to build complex web applications. In this tutorial, you’ll jump in and learn Django by example. You’ll follow the steps to create a fully functioning web application and, along the way, learn some of the most important features of the framework and how they work together.

In later posts in this series, you’ll see how to build more complex websites using even more of Django’s features than you’ll cover in this tutorial.

By the end of this tutorial, you will be able to:

  • Understand what Django is and why it’s a great web framework
  • Understand the architecture of a Django site and how it compares with other frameworks
  • Set up a new Django project and app
  • Build a Personal Portfolio Website with Django

Free Bonus: Click here to get access to a free Django Learning Resources Guide (PDF) that shows you tips and tricks as well as common pitfalls to avoid when building Python + Django web applications.

Why You Should Learn Django

There are endless web development frameworks out there, so why should you learn Django over any of the others? First of all, it’s written in Python, one of the most readable and beginner-friendly programming languages out there.

Note: This tutorial assumes an intermediate knowledge of the Python language. If you’re new to programming with Python, check out some of our beginner tutorials or the introductory course.

The second reason you should learn Django is the scope of its features. If you need to build a website, you don’t need to rely on any external libraries or packages if you choose Django. This means that you don’t need to learn how to use anything else, and the syntax is seamless as you’re using only one framework.

There’s also the added benefit that you don’t need to worry that updating one library or framework will render others that you’ve installed useless.

If you do find yourself needing to add extra features, there are a range of external libraries that you can use to enhance your site.

One of the great things about the Django framework is its in-depth documentation. It has detailed documentation on every aspect of Django and also has great examples and even a tutorial to get you started.

There’s also a fantastic community of Django developers, so if you get stuck there’s almost always a way forward by either checking the docs or asking the community.

Django is a high-level web application framework with loads of features. It’s great for anyone new to web development due to its fantastic documentation, and particularly if you’re also familiar with Python.

The Structure of a Django Website

A Django website consists of a single project that is split into separate apps. The idea is that each app handles a self-contained function that the site needs to perform. As an example, imagine an application like Instagram. There are several different functions that need to be performed:

  • User management: Login, logout, register, and so on
  • The image feed: Uploading, editing, and displaying images
  • Private messaging: Private messages between users and notifications

These are each separate pieces of functionality, so if this were a Django site, then each piece of functionality should be a different Django app inside a single Django project.

The Django project holds some configurations that apply to the project as a whole, such as project settings, URLs, shared templates and static files. Each application can have its own database and has its own functions to control how the data is displayed to the user in HTML templates.

Each application also has its own URLs as well as its own HTML templates and static files, such as JavaScript and CSS.

Django apps are structured so that there is a separation of logic. It supports the Model-View-Controller Pattern, which is the architecture on which most web frameworks are built. The basic principle is that in each application there are three separate files that handle the three main pieces of logic separately:

  • Model defines the data structure. This is usually a database and is the base layer to an application.
  • View displays some or all of the data to the user with HTML and CSS.
  • Controller handles how the database and the view interact.

If you want to learn more about the MVC pattern, then check out Model-View-Controller (MVC) Explained – With Legos.

In Django, the architecture is slightly different. Although based upon the MVC pattern, Django handles the controller part itself. There’s no need to define how the database and views interact. It’s all done for you!

The pattern Django utilizes is called the Model-View-Tempalate (MVT) pattern. The view and template in the MVT pattern make up the view in the MVC pattern. All you need to do is add some URL configurations to map the views to, and Django handles the rest!

A Django site starts off as a project and is built up with a number of applications that each handle separate functionality. Each app follows the Model-View-Template pattern. Now that you’re familiar with the structure of a Django site, let’s have a look at what you’re going to build!

What You’re Going to Build

Before you get started with any web development project, it’s a good idea to come up with a plan of what you’re going to build. In this tutorial, we are going to build an application with the following features:

  • A fully functioning blog: If you’re looking to demonstrate your coding ability, a blog is a great way to do that. In this application, you will be able to create, update, and delete blog posts. Posts will have categories that can be used to sort them. Finally, users will be able to leave comments on posts.

  • A portfolio of your work: You can showcase previous web development projects here. You’ll build a gallery style page with clickable links to projects that you’ve completed.

Note: Before you get started, you can pull down the source code and follow along with the tutorial.

If you prefer to follow along by writing the code yourself, don’t worry. I’ve referenced the relevant parts of the source code throughout so you can refer back to it.

We won’t be using any external Python libraries in this tutorial. One of the great things about Django is that it has so many features that you don’t need to rely on external libraries. However, we will add Bootstrap 4 styling in the templates.

By building these two apps, you’ll learn the basics of Django models, view functions, forms, templates, and the Django admin page. With knowledge of these features, you’ll be able to go away and build loads more applications. You’ll also have the tools to learn even more and build sophisticated Django sites.

Hello, World!

Now that you know the structure of a Django application, and what you are about to build, we’re going to go through the process of creating an application in Django. You’ll extend this later into your personal portfolio application.

Set Up Your Development Environment

Whenever you are starting a new web development project, it’s a good idea to first set up your development environment. Create a new directory for your project to live in, and cd into it:

$   mkdir rp-portfolio $   cd rp-portfolio 

Once your inside the main directory, it’s a good idea to create a virtual environment to manage dependencies. There are many different ways to set up virtual environments, but here you’re going to use venv:

$   python3 -m venv venv 

This command will create a folder venv in your working directory. Inside this directory, you’ll find several files including a copy of the Python standard library. Later, when you install new dependencies, they will also be stored in this directory. Next, you need to activate the virtual environment by running the following command:

$   source venv/bin/activate 

Note: If you’re not using bash shell, you might need to use a different command to activate your virtual environment. For example, on windows you need this command:

C:\> venv\Scripts\activate.bat 

You’ll know that your virtual environment has been activated, because your console prompt in the terminal will change. It should look something like this:

(venv) $   

Note: Your virtual environment directory doesn’t have to be called venv. If you want to create one under a different name, for example my_venv, just replace with the second venv with my_venv.

Then, when activating your virtual environment, replace venv with my_venv again. The prompt will also now be prefixed with (my_venv).

Now that you’ve created a virtual environment, it’s time to install Django. You can do this using pip:

(venv) $   pip install Django 

Once you’ve set up the virtual environment and installed Django, you can now dive in to creating the application.

Create a Django Project

As you saw in the previous section, a Django web application is made up of a project and its constituent apps. Making sure you’re in the rp_portfolio directory, and you’ve activated your virtual environment, run the following command to create the project:

$   django-admin startproject personal_portfolio 

This will create a new directory personal_portfolio. If you cd into this new directory you’ll see another directory called personal_portfolio and a file called manage.py. Your directory structure should look something like this:

rp-portfolio/ β”‚ β”œβ”€β”€ personal_portfolio/ β”‚   β”œβ”€β”€ personal_portfolio/ β”‚   β”‚   β”œβ”€β”€ __init__.py β”‚   β”‚   β”œβ”€β”€ settings.py β”‚   β”‚   β”œβ”€β”€ urls.py β”‚   β”‚   └── wsgi.py β”‚   β”‚ β”‚   └── manage.py β”‚ └── venv/ 

Most of the work you do will be in that first personal_portfolio directory. To save having to cd through several directories each time you come to work on your project, it can be helpful to reorder this slightly by moving all the files up a directory. While you’re in the rp-portfolio directory, run the following commands:

$   mv personal_portfolio/manage.py ./ $   mv personal_portfolio/personal_portfolio/* personal_portfolio $   rm -r personal_portfolio/personal_portfolio/ 

You should end up with something like this:

rp-portfolio/ β”‚ β”œβ”€β”€ personal_portfolio/ β”‚   β”œβ”€β”€ __init__.py β”‚   β”œβ”€β”€ settings.py β”‚   β”œβ”€β”€ urls.py β”‚   └── wsgi.py β”‚ β”œβ”€β”€ venv/ β”‚ └── manage.py 

Once your file structure is set up, you can now start the server and check that your set up was successful. In the console, run the following command:

$   python manage.py runserver 

Then, in your browser go to localhost:8000, and you should see the following:

Initial view of Django site

Congratulations, you’ve created a Django site! The source code for this part of the tutorial can be found on GitHub. The next step is to create apps so that you can add views and functionality to your site.

Create a Django Application

For this part of the tutorial, we’ll create an app called hello_world, which you’ll subsequently delete as its not necessary for our personal portfolio site.

To create the app, run the following command:

$   python manage.py startapp hello_world 

This will create another directory called hello_world with several files:

  • __init__.py tells Python to treat the directory as a Python package.
  • admin.py contains settings for the Django admin pages.
  • apps.py contains settings for the application configuration.
  • models.py contains a series of classes that Django’s ORM converts to database tables.
  • tests.py contains test classes.
  • views.py contains functions and classes that handle what data is displayed in the HTML templates.

Once you’ve created the app, you need to install it in your project. In rp-portfolio/settings.py, add the following line of code under INSTALLED_APPS:

INSTALLED_APPS = [     'django.contrib.admin',     'django.contrib.auth',     'django.contrib.contenttypes',     'django.contrib.sessions',     'django.contrib.messages',     'django.contrib.staticfiles',     'hello_world', ] 

That line of code means that your project now knows that the app you just created exists. The next step is to create a view so that you can display something to a user.

Create a View

Views in Django are a collection of functions or classes inside the views.py file in your app directory. Each function or class handles the logic that gets processed each time a different URL is visited.

Navigate to the views.py file in the hello_world directory. There’s already a line of code in there that imports render(). Add the following code:

from django.shortcuts import render  def hello_world(request):     return render(request, 'hello_world.html', {}) 

In this piece of code, you’ve defined a view function called hello_world(). When this function is called, it will render an HTML file called hello_world.html. That file doesn’t exist yet, but we’ll create it soon.

The view function takes one argument, request. This object is an HttpRequestObject that is created whenever a page is loaded. It contains information about the request, such as the method, which can take several values including GET and POST.

Now that you’ve created the view function, you need to create the HTML template to display to the user. render() looks for HTML templates inside a directory called templates inside your app directory. Create that directory and subsequently a file named hello_world.html inside it:

$   mkdir hello_world/templates/ $   touch hello_world/templates/hello_world.html 

Add the following lines of HTML to your file:

<h1>Hello, World!</h1> 

You’ve now created a function to handle your views and templates to display to the user. The final step is to hook up your URLs so that you can visit the page you’ve just created. Your project has a module called urls.py in which you need to include a URL configuration for the hello_world app. Inside personal_portfolio/urls.py, add the following:

from django.contrib import admin from django.urls import path, include  urlpatterns = [     path('admin/', admin.site.urls),     path('', include('hello_world.urls')), ] 

This looks for a module called urls.py inside the hello_world application and registers any URLs defined there. Whenever you visit the root path of your URL (localhost:8000), the hello_world application’s URLs will be registered. The hello_world.urls module doesn’t exist yet, so you’ll need to create it:

$   touch hello_world/urls.py 

Inside this module, we need to import the path object as well as our app’s views module. Then we want to create a list of URL patterns that correspond to the various view functions. At the moment, we have only created one view function, so we need only create one URL:

from django.urls import path from hello_world import views  urlpatterns = [     path('', views.hello_world, name='hello_world'), ] 

Now, when you restart the server and visit localhost:8000, you should be able to see the HTML template you created:

Hello, World! view of Django site

Congratulations, again! You’ve created your first Django app and hooked it up to your project. Don’t forget to check out the source code for this section and the previous one. The only problem now is that it doesn’t look very nice. In the next section, we’re going to add bootstrap styles to your project to make it prettier!

Add Bootstrap to Your App

If you don’t add any styling, then the app you create isn’t going to look too nice. Instead of going into CSS styling with this tutorial, we’ll just cover how to add bootstrap styles to your project. This will allow us to improve the look of the site without too much effort.

Before we get started with the Bootstrap styles, we’ll create a base template that we can import to each subsequent view. This template is where we’ll subsequently add the Bootstrap style imports.

Create another directory called templates, this time inside personal_portfolio, and a file called base.html, inside the new directory:

$   mkdir personal_portfolio/templates/ $   touch personal_portfolio/templates/base.html 

We create this additional templates directory to store HTML templates that will be used in every Django app in the project. As you saw previously, each Django project can consist of multiple apps that handle separated logic, and each app contains its own templates directory to store HTML templates related to the application.

This application structure works well for the back end logic, but we want our entire site to look consistent on the front end. Instead of having to import Bootstrap styles into every app, we can create a template or set of templates that are shared by all the apps. As long as Django knows to look for templates in this new, shared directory it can save a lot of repeated styles.

Inside this new file (personal_portfolio/templates/base.html), add the following lines of code:

{% block page_content %}{% endblock %} 

Now, in hello_world/templates/hello_world.html, we can extend this base template:

{% extends "base.html" %}  {% block page_content %} <h1>Hello, World!</h1> {% endblock %} 

What happens here is that any HTML inside the page_content block gets added inside the same block in base.html.

To install Bootstrap in your app, you’ll use the Bootstrap CDN. This is a really simple way to install Bootstrap that just involves adding a few lines of code to base.html. Check out the source code to see how to add the CDN links to your project.

All future templates that we create will extend base.html so that we can include Bootstrap styling on every page without having to import the styles again.

Before we can see our new styled application, we need to tell our Django project that base.html exists. The default settings register template directories in each app, but not in the project directory itself. In personal_portfolio/settings.py, update TEMPLATES:

TEMPLATES = [     {         "BACKEND": "django.template.backends.django.DjangoTemplates",         "DIRS": ["personal_portfolio/templates/"],         "APP_DIRS": True,         "OPTIONS": {             "context_processors": [                 "django.template.context_processors.debug",                 "django.template.context_processors.request",                 "django.contrib.auth.context_processors.auth",                 "django.contrib.messages.context_processors.messages",             ]         },     } ] 

Now, when you visit localhost:8000, you should see that the page has been formatted with slightly different styling:

Hello, World! view of Django site with Bootstrap styles

Whenever you want create templates or import scripts that you intend to use in all your Django apps inside a project, you can add them to this project-level directory and extend them inside your app templates.

Adding templates is the last stage to building your Hello, World! Django site. You learned how the Django templating engine works and how to create project-level templates that can be shared by all the apps inside your Django project.

In this section, you learned how to create a simple Hello, World! Django site by creating a project with a single app. In the next section, you’ll create another application to showcase web development projects, and you’ll learn all about models in Django!

The source code for this section can be found on GitHub.

Showcase Your Projects

Any web developer looking to create a portfolio needs a way to show off projects they have worked on. That’s what you’ll be building now. You’ll create another Django app called projects that will hold a series of sample projects that will be displayed to the user. Users can click on projects and see more information about your work.

Before we build the projects app, let’s first delete the hello_world application. All you need to do is delete the hello_world directory and remove the line "hello_world", from INSTALLED_APPS in settings.py:

INSTALLED_APPS = [     'django.contrib.admin',     'django.contrib.auth',     'django.contrib.contenttypes',     'django.contrib.sessions',     'django.contrib.messages',     'django.contrib.staticfiles',     'hello_world',  # Delete this line ] 

Finally, you need to remove the URL path created in personal_portfolio/urls.py:

from django.contrib import admin from django.urls import path, include  urlpatterns = [     path('admin/', admin.site.urls),     path('', include('hello_world.urls')),  # Delete this line ] 

Now that you’ve removed the hello_world app, we can create the projects app. Making sure you’re in the rp-portfolio directory, run the following command in your console:

$   python manage.py startapp projects 

This will create a directory named projects. The files created are the same as those created when we set up the hello_world application. In order to hook up our app, we need to add it into INSTALLED_APPS in settings.py:

INSTALLED_APPS = [     'django.contrib.admin',     'django.contrib.auth',     'django.contrib.contenttypes',     'django.contrib.sessions',     'django.contrib.messages',     'django.contrib.staticfiles',     'projects', ] 

Check out the source code for this section on GitHub. We’re not going to worry about URLs for this application just yet. Instead, we’re going to focus on building a Project model.

Projects App: Models

If you want to store data to display on a website, then you’ll need a database. Typically, if you want to create a database with tables and columns within those tables, you’ll need to use SQL to manage the database. But when you use Django, you don’t need to learn a new language because it has a built-in Object Relational Mapper (ORM).

An ORM is a program that allows you to create classes that correspond to database tables. Class attributes correspond to columns, and instances of the classes correspond to rows in the database. So, instead of learning a whole new language to create our database and its tables, we can just write some Python classes.

When you’re using an ORM, the classes you build that represent database tables are referred to as models. In Django, they live in the models.py module of each Django app.

In your projects app, you’ll only need one table to store the different projects you’ll display to the user. That means you’ll only need to create one model in models.py.

The model you’ll create will be called Project and will have the following fields:

  • title will be a short string field to hold the name of your project.
  • description will be a larger string field to hold a longer piece of text.
  • technology will be a string field, but its contents will be limited to a select number of choices.
  • image will be an image field that holds the file path where the image is stored.

To create this model, we’ll create a new class in models.py and add the following in our fields:

from django.db import models  class Project(models.Model):     title = models.CharField(max_length=100)     description = models.TextField()     technology = models.CharField(max_length=20)     image = models.FilePathField(path="/img") 

Django models come with many built-in model field types. We’ve only used three in this model. CharField is used for short strings and specifies a maximum length.

TextField is similar to CharField but can be used for longer form text as it doesn’t have a maximum length limit. Finally, FilePathField also holds a string but must point to a file path name.

Now that we’ve created our Project class, we need Django to create the database. By default, the Django ORM creates databases in SQLite, but you can use other databases that use the SQL language, such as PostgreSQL or MySQL, with the Django ORM.

To start the process of creating our database, we need to create a migration. A migration is a file containing a Migration class with rules that tell Django what changes need to be made to the database. To create the migration, type the following command in the console, making sure you’re in the rp-portfolio directory:

$   python manage.py makemigrations projects Migrations for 'projects':   projects/migrations/0001_initial.py     - Create model Project 

You should see that a file projects/migrations/0001_initial.py has been created in the projects app. Check out that file in the source code to make sure your migration is correct.

Now that you’ve create a migration file, you need to apply the migrations set out in the migrations file and create your database using the migrate command:

$   python manage.py migrate projects Operations to perform:   Apply all migrations: projects Running migrations:   Applying projects.0001_initial... OK 

Note: When running both the makemigrations and migrate commands, we added projects to our command. This tells Django to only look at models and migrations in the projects app. Django comes with several models already created.

If you run makemigrations and migrate without the projects flag, then all migrations for all the default models in your Django projects will be created and applied. This is not a problem, but for the purposes of this section, they are not needed.

You should also see that a file called db.sqlite3 has been created in the root of your project. Now your database is set up and ready to go. You can now create rows in your table that are the various projects you want to show on your portfolio site.

To create instances of our Project class, we’re going to have to use the Django shell. The Django shell is similar to the Python shell but allows you to access the database and create entries. To access the Django shell, we use another Django management command:

$   python manage.py shell 

Once you’ve accessed the shell, you’ll notice that the command prompt will change from $ to >>>. You can then import your models:

>>>

>>> from projects.models import Project 

We’re first going to create a new project with the following attributes:

  • name: My First Project
  • description: A web development project.
  • technology: Django
  • image: img/project1.png

To do this, we create an instance of the Project class in the Django shell:

>>>

>>> p1 = Project( ...     title='My First Project', ...     description='A web development project.', ...     technology='Django', ...     image='img/project1.png' ... ) >>> p1.save() 

This creates a new entry in your projects table and saves it to the database. Now you have created a project that you can display on your portfolio site.

The final step in this section is to create two more sample projects:

>>>

>>> p2 = Project( ...     title='My Second Project', ...     description='Another web development project.', ...     technology='Flask', ...     image='img/project2.png' ... ) >>> p2.save() >>> p3 = Project( ...     title='My Third Project', ...     description='A final development project.', ...     technology='Django', ...     image='img/project3.png' ... ) >>> p3.save() 

Well done for reaching the end of this section! You now know how to create models in Django and build migration files so that you can translate these model classes into database tables. You’ve also used the Django shell to create three instances of your model class.

In the next section, we’ll take these three projects you created and create a view function to display them to users on a web page. You can find the source code for this section of the tutorial on GitHub.

Projects App: Views

Now you’ve created the projects to display on your portfolio site, you’ll need to create view functions to send the data from the database to the HTML templates.

In the projects app, you’ll create two different views:

  1. An index view that shows a snippet of information about each project
  2. A detail view that shows more information on a particular topic

Let’s start with the index view, as the logic is slightly simpler. Inside views.py, you’ll need to import the Project class from models.py and create a function project_index() that renders a template called project_index.html. In the body of this function, you’ll make a Django ORM query to select all objects in the Project table:

 1 from django.shortcuts import render  2 from projects.models import Project  3   4 def project_index(request):  5     projects = Project.objects.all()  6     context = {  7         'projects': projects  8     }  9     return render(request, 'project_index.html', context) 

There’s quite a lot going on in this code block, so let’s break it down.

In line 6, you perform a query. A query is simply a command that allows you to create, retrieve, update, or delete objects (or rows) in your database. In this case, you’re retrieving all objects in the projects table.

A database query returns a collection of all objects that match the query, known as a Queryset. In this case, you want all objects in the table, so it will return a collection of all projects.

In line 7 of the code block above, we define a dictionary context. The dictionary only has one entry projects to which we assign our Queryset containing all projects. The context dictionary is used to send information to our template. Every view function you create needs to have a context dictionary.

In line 10, context is added as an argument to render(). Any entries in the context dictionary are available in the template, as long as the context argument is passed to render(). You’ll need to create a context dictionary and pass it to render in each view function you create.

We also render a template named project_index.html, which doesn’t exist yet. Don’t worry about that for now. You’ll create the templates for these views in the next section.

Next, you’ll need to create the project_detail() view function. This function will need an additional argument: the id of the project that’s being viewed.

Otherwise, the logic is similar:

13 def project_detail(request, pk): 14     project = Project.objects.get(pk=pk) 15     context = { 16         'project': project 17     } 18     return render(request, 'project_detail.html', context) 

In line 14, we perform another query. This query retrieves the project with primary key, pk, equal to that in the function argument. We then assign that project in our context dictionary, which we pass to render(). Again, there’s a template project_detail.html, which we have yet to create.

Once your view functions are created, we need to hook them up to URLs. We’ll start by creating a file projects/urls.py to hold the URL configuration for the app. This file should contain the following code:

 1 from django.urls import path  2 from . import views  3   4 urlpatterns = [  5     path("", views.project_index, name="project_index"),  6     path("<int:pk>/", views.project_detail, name="project_detail"),  7 ] 

In line 5, we hook up the root URL of our app to the project_index view. It is slightly more complicated to hook up the project_detail view. To do this, we want the URL to be /1, or /2, and so on, depending on the pk of the project.

The pk value in the URL is the same pk passed to the view function, so you need to dynamically generate these URLs depending on which project you want to view. To do this, we used the <int:pk> notation. This just tells Django that the value passed in the URL is an integer, and its variable name is pk.

With those now set up, we need to hook these URLs up to the project URLs. In personal_portfolio/urls.py, add the following highlighted line of code:

from django.contrib import admin from django.urls import path, include  urlpatterns = [     path("admin/", admin.site.urls),     path("projects/", include("projects.urls")), ] 

This line of code includes all the URLs in the projects app but means they are accessed when prefixed by projects/. There are now two full URLs that can be accessed with our project:

  • localhost:8000/projects: The project index page
  • localhost:8000/projects/3: The detail view for the project with pk=3

These URLs still won’t work properly because we don’t have any HTML templates. But our views and logic are up and running so all that’s left to do is create those templates. If you want to check your code, take a look at the source code for this section.

Projects App: Templates

Phew! You’re nearly there with this app. Our final step is to create two templates:

  1. The project_index template
  2. The project_detail template

As we’ve added Bootstrap styles to our application, we can use some pre-styled components to make the views look nice. Let’s start with the project_index template.

For the project_index template, you’ll create a grid of Bootstrap cards, with each card displaying details of the project. Of course, we don’t know how many projects there are going to be. In theory, there could be hundreds to display.

We don’t want to have to create 100 different Bootstrap cards and hard-code in all the information to each project. Instead, we’re going to use a feature of the Django template engine: for loops.

Using this feature, you’ll be able to loop through all the projects and create a card for each one. The for loop syntax in the Django template engine is as follows:

{% for project in projects %} {# Do something... #} {% endfor %} 

Now that you know how for loops work, you can add the following code to a file named projects/templates/project_index.html:

 1 {% extends "base.html" %}  2 {% load static %}  3 {% block page_content %}  4 <h1>Projects</h1>  5 <div class="row">  6 {% for project in projects %}  7     <div class="col-md-4">  8         <div class="card mb-2">  9             <img class="card-img-top" src="{% static project.image %}"> 10             <div class="card-body"> 11                 <h5 class="card-title">{{ project.title }}</h5> 12                 <p class="card-text">{{ project.description }}</p> 13                 <a href="{% url 'project_detail' project.pk %}" 14                    class="btn btn-primary"> 15                     Read More 16                 </a> 17             </div> 18         </div> 19     </div> 20     {% endfor %} 21 </div> 22 {% endblock %} 

There’s a lot of Bootstrap HTML here, which is not the focus of this tutorial. Feel free to copy and paste and take a look at the Bootstrap docs if you’re interested in learning more. Instead of focusing on the Bootstrap, there are a few things to highlight in this code block.

In line 1, we extend base.html as we did in the Hello, World! app tutorial. I’ve added some more styling to this file to include a navigation bar and so that all the content is contained in a Bootstrap container. The changes to base.html can be seen in the source code on GitHub.

On line 2, we include a {% load static %} tag to include static files such as images. Remember back in the section on Django models, when you created the Project model. One of its attributes was a filepath. That filepath is where we’re going to store the actual images for each project.

Django automatically registers static files stored in a directory named static/ in each application. Our image file path names were of the structure: img/<photo_name>.png.

When loading static files, Django looks in the static/ directory for files matching a given filepath within static/. So, we need to create a directory named static/ with another directory named img/ inside. Inside img/, you can copy over the images from the source code on GitHub.

On line 6, we begin the for loop, looping over all projects passed in by the context dictionary.

Inside this for loop, we can access each individual project. To access the project’s attributes, you can use dot notation inside double curly brackets. For example, to access the project’s title, you use {{ project.title }}. The same notation can be used to access any of the project’s attributes.

On line 9, we include our project image. Inside the src attribute, we add the code {% static project.image %}. This tells Django to look inside the static files to find a file matching project.image.

The final point that we need to highlight is the link on line 13. This is the link to our project_detail page. Accessing URLs in Django is similar to accessing static files. The code for the URL has the following form:

{% url '<url path name>' <view_function_arguments> %} 

In this case, we are accessing a URL path named project_detail, which takes integer arguments corresponding to the pk number of the project.

With all that in place, if you start the Django server and visit localhost:8000/projects, then you should see something like this:

project index view

With the project_index.html template in place, it’s time to create the project_detail.html template. The code for this template is below:

{% extends "base.html" %} {% load static %}  {% block page_content %} <h1>{{ project.title }}</h1> <div class="row">     <div class="col-md-8">         <img src="{% static project.image %}" alt="" width="100%">     </div>     <div class="col-md-4">         <h5>About the project:</h5>         <p>{{ project.description }}</p>         <br>         <h5>Technology used:</h5>         <p>{{ project.technology }}</p>     </div> </div> {% endblock %} 

The code in this template has the same functionality as each project card in the project_index.html template. The only difference is the introduction of some Bootstrap columns.

If you visit localhost:8000/projects/1, you should see the detail page for that first project you created:

project detail view

In this section, you learned how to use models, views, and templates to create a fully functioning app for your personal portfolio project. Check out the source code for this section on GitHub.

In the next section, you’ll build a fully functioning blog for your site, and you’ll also learn about the Django admin page and forms.

Share Your Knowledge With a Blog

A blog is a great addition to any personal portfolio site. Whether you update it monthly or weekly, it’s a great place to share your knowledge as you learn. In this section, you’re going to build a fully functioning blog that will allow you to perform the following tasks:

  • Create, update, and delete blog posts
  • Display posts to the user as either an index view or a detail view
  • Assign categories to posts
  • Allow users to comment on posts

You’ll also learn how to use the Django Admin interface, which is where you’ll create, update, and delete posts and categories as necessary.

Before you get into building out the functionality of this part of your site, create a new Django app named blog. Don’t delete projects. You’ll want both apps in your Django project:

$   python manage.py startapp blog 

This may start to feel familiar to you, as its your third time doing this. Don’t forget to add blog to your INSTALLED_APPS in personal_porfolio/settings.py:

INSTALLED_APPS = [     "django.contrib.admin",     "django.contrib.auth",     "django.contrib.contenttypes",     "django.contrib.sessions",     "django.contrib.messages",     "django.contrib.staticfiles",     "projects",     "blog", ] 

Hold off on hooking up the URLs for now. As with the projects app, you’ll start by adding your models.

Blog App: Models

The models.py file in this app is much more complicated than in the projects app.

You’re going to need three separate database tables for the blog:

  1. Post
  2. Category
  3. Comment

These tables need to be related to one another. This is made easier because Django models come with fields specifically for this purpose.

Below is the code for the Category and Post models:

 1 from django.db import models  2   3 class Category(models.Model):  4     name = models.CharField(max_length=20)  5   6 class Post(models.Model):  7     title = models.CharField(max_length=255)  8     body = models.TextField()  9     created_on = models.DateTimeField(auto_now_add=True) 10     last_modified = models.DateTimeField(auto_now=True) 11     categories = models.ManyToManyField('Category', related_name='posts') 

The Category model is very simple. All that’s needed is a single CharField in which we store the name of the category.

The title and body fields on the Post model are the same field types as you used in the Project model. We only need a CharField for the title as we only want a short string for the post title. The body needs to be a long-form piece of text, so we use a TextField.

The next two fields, created_on and last_modified, are Django DateTimeFields. These store a datetime object containing the date and time when the post was created and modified respectively.

On line 11, the DateTimeField takes an argument auto_now_add=True. This assigns the current date and time to this field whenever an instance of this class is created.

On line 12, the DateTimeField takes an argument auto_now=True. This assigns the current date and time to this field whenever an instance of this class is saved. That means whenever you edit an instance of this class, the date_modified is updated.

The final field on the post model is the most interesting. We want to link our models for categories and posts in such a way that many categories can be assigned to many posts. Luckily, Django makes this easier for us by providing a ManytoManyField field type. This field links the Post and Category models and allows us to create a relationship between the two tables.

The ManyToManyField takes two arguments. The first is the model with which the relationship is, in this case its Category. The second allows us to access the relationship from a Category object, even though we haven’t added a field there. By adding a related_name of posts, we can access category.posts to give us a list of posts with that category.

The third and final model we need to add is Comment. We’ll use another relationship field similar the ManyToManyField that relates Post and Category. However, we only want the relationship to go one way: one post should have many comments.

You’ll see how this works after we define the Comment class:

16 class Comment(models.Model): 17     author = models.CharField(max_length=60) 18     body = models.TextField() 19     created_on = models.DateTimeField(auto_now_add=True) 20     post = models.ForeignKey('Post', on_delete=models.CASCADE) 

The first three fields on this model should look familiar. There’s an author field for users to add a name or alias, a body field for the body of the comment, and a created_on field that is identical to the created_on field on the Post model.

On line 20, we use another relational field, the ForeignKey field. This is similar to the ManyToManyField but instead defines a many to one relationship. The reasoning behind this is that many comments can be assigned to one post. But you can’t have a comment that corresponds to many posts.

The ForeignKey field takes two arguments. The first is the other model in the relationship, in this case, Post. The second tells Django what to do when a post is deleted. If a post is deleted, then we don’t want the comments related to it hanging around. We, therefore, want to delete them as well, so we add the argument on_delete=models.CASCADE.

Once you’ve created the models, you can create the migration files with makemigrations:

$   python manage.py makemigrations blog 

The final step is to migrate the tables. This time, don’t add the app-specific flag. Later on, you’ll need the User model that Django creates for you:

$   python manage.py migrate 

Now that you’ve created the models, we can start to add some posts and categories. You won’t be doing this from the command line as you did with the projects, as typing out a whole blog post into the command line would be unpleasant to say the least!

Instead, you’ll learn how to use the Django Admin, which will allow you to create instances of your model classes in a nice web interface.

Don’t forget that you can check out the source code for this section on GitHub before moving onto the next section.

Blog App: Django Admin

The Django Admin is a fantastic tool and one of the great benefits of using Django. As you’re the only person who’s going to be writing blog posts and creating categories, there’s no need to create a user interface to do so.

On the other hand, you don’t want to have to write blog posts in the command line. This is where the admin comes in. It allows you to create, update, and delete instances of your model classes and provides a nice interface for doing so.

Before you can access the admin, you need to add yourself as a superuser. This is why, in the previous section, you applied migrations project-wide as opposed to just for the app. Django comes with built-in user models and a user management system that will allow you to login to the admin.

To start off, you can add yourself as superuser using the following command:

$   python manage.py createsuperuser 

You’ll then be prompted to enter a username followed by your email address and password. Once you’ve entered the required details, you’ll be notified that the superuser has been created. Don’t worry if you make a mistake since you can just start again:

Username (leave blank to use 'jasmine'): jfiner Email address: jfiner@example.com Password: Password (again): Superuser created successfully. 

Navigate to localhost:8000/admin and log in with the credentials you just used to create a superuse. You’ll see a page similar to the one below:

The Default Django Admin

The User and Groups models should appear, but you’ll notice that there’s no reference to the models you’ve created yourself. That’s because you need to register them inside the admin.

In the blog directory, open the file admin.py and type the following lines of code:

 1 from django.contrib import admin  2 from blog.models import Post, Category  3   4 class PostAdmin(admin.ModelAdmin):  5     pass  6   7 class CategoryAdmin(admin.ModelAdmin):  8     pass  9  10 admin.site.register(Post, PostAdmin) 11 admin.site.register(Category, CategoryAdmin) 

On line 2, you import the models you want to register on the admin page.

Note: We’re not adding the comments to the admin. That’s because it’s not usually necessary to edit or create comments yourself.

If you wanted to add a feature where comments are moderated, then go ahead and add the Comments model too. The steps to do so are exactly the same!

On line 5 and line 9, you define empty classes PostAdmin and CategoryAdmin. For the purposes of this tutorial, you don’t need to add any attributes or methods to these classes. They are used to customize what is shown on the admin pages. For this tutorial, the default configuration is enough.

The last two lines are the most important. These register the models with the admin classes. If you now visit localhost:8000/admin, then you should see that the Post and Category models are now visible:

Django Admin with Posts and Categories

If you click into Posts or Categorys, you should be able to add new instances of both models. I like to add the text of fake blog posts by using lorem ipsum dummy text.

Create a couple of fake posts and assign them fake categories before moving onto the next section. That way, you’ll have posts you can view when we create our templates.

Don’t forget to check out the source code for this section before moving on to building out the views for our app.

Blog App: Views

You’ll need to create three view functions in the views.py file in the blog directory:

  • blog_index will display a list of all your posts.
  • blog_detail will display the full post as well as comments and a form to allow users to create new comments.
  • blog_category will be similar to blog_index, but the posts viewed will only be of a specific category chosen by the user.

The simplest view function to start with is blog_index(). This will be very similar to the project_index() view from your project app. You’ll just query the Post models and retrieve all its objects:

 1 from django.shortcuts import render  2 from blog.models import Post  3   4 def blog_index(request):  5     posts = Post.objects.all().order_by('-created_on')  6     context = {  7         "posts": posts,  8     }  9     return render(request, "blog_index.html", context) 

On line 3, you import the Post model, and on line 6 inside the view function, you obtain a Queryset containing all the posts in the database. order_by() orders the Queryset according to the argument given. The minus sign tells Django to start with the largest value rather than the smallest. We use this, as we want the posts to be ordered with the most recent post first.

Finally, you define the context dictionary and render the template. Don’t worry about creating it yet. You’ll get to creating those in the next section.

Next, you can start to create the blog_category() view. The view function will need to take a category name as an argument and query the Post database for all posts that have been assigned the given category:

13 def blog_category(request, category): 14     posts = Post.objects.filter( 15         categories__name__contains=category 16     ).order_by( 17         '-created_on' 18     ) 19     context = { 20         "category": category, 21         "posts": posts 22     } 23     return render(request, "blog_category.html", context) 

On line 14, you’ve used a Django Queryset filter. The argument of the filter tells Django what conditions need to be met for an object to be retrieved. In this case, we only want posts whose categories contain the category with the name corresponding to that given in the argument of the view function. Again, you’re using order_by() to order posts starting with the most recent.

We then add these posts and the category to the context dictionary, and render our template.

The last view function to add is blog_detail(). This is more complicated as we are going to include a form. Before you add the form, just set up the view function to show a specific post with a comment associated with it. This function will be almost equivalent to the project_detail() view function in the projects app:

21 def blog_detail(request, pk): 22     post = Post.objects.get(pk=pk) 23     comments = Comment.objects.filter(post=post) 24     context = { 25         "post": post, 26         "comments": comments, 27     } 28  29     return render(request, "blog_detail.html", context) 

The view function takes a pk value as an argument and, on line 22, retrieves the object with the given pk.

On line 23, we retrieve all the comments assigned to the given post using Django filters again.

Lastly, add both post and comments to the context dictionary and render the template.

To add a form to the page, you’ll need to create another file in the blog directory named forms.py. Django forms are very similar to models. A form consists of a class where the class attributes are form fields. Django comes with some built-in form fields that you can use to quickly create the form you need.

For this form, the only fields you’ll need are author, which should be a CharField, and body, which can also be a CharField.

Note: If the CharField of your form corresponds to a model CharField, make sure both have the same max_length value.

blog/forms.py should contain the following code:

from django import forms  class CommentForm(forms.Form):     author = forms.CharField(         max_length=60,         widget=forms.TextInput(attrs={             "class": "form-control",             "placeholder": "Your Name"         })     )     body = forms.CharField(widget=forms.Textarea(         attrs={             "class": "form-control",             "placeholder": "Leave a comment!"         })     ) 

You’ll also notice an argument widget has been passed to both the fields. The author field has the forms.TextInput widget. This tells Django to load this field as an HTML text input element in the templates. The body field uses a forms.TextArea widget instead, so that the field is rendered as an HTML text area element.

These widgets also take an argument attrs, which is a dictionary and allows us to specify some CSS classes, which will help with formatting the template for this view later. It also allows us to add some placeholder text.

When a form is posted, a POST request is sent to the server. So, in the view function, we need to check if a POST request has been received. We can then create a comment from the form fields. Django comes with a handy is_valid() on its forms, so we can check that all the fields have been entered correctly.

Once you’ve created the comment from the form, you’ll need to save it using save() and then query the database for all the comments assigned to the given post. Your view function should contain the following code:

21 def blog_detail(request, pk): 22     post = Post.objects.get(pk=pk) 23  24     form = CommentForm() 25     if request.method == 'POST': 26         form = CommentForm(request.POST) 27         if form.is_valid(): 28             comment = Comment( 29                 author=form.cleaned_data["author"], 30                 body=form.cleaned_data["body"], 31                 post=post 32             ) 33             comment.save() 34  35     comments = Comment.objects.filter(post=post) 36     context = { 37         "post": post, 38         "comments": comments, 39         "form": CommentForm(), 40     } 41     return render(request, "blog_detail.html", context) 

On line 25, we create an instance of our form class. Don’t forget to import your form at the beginning of the file:

from . import CommentForm 

We then go on to check if a POST request has been received. If it has, then we create a new instance of our form, populated with the data entered into the form.

The form is then validated using is_valid(). If the form is valid, a new instance of Comment is created. You can access the data from the form using form.cleaned_data, which is a dictionary.

They keys of the dictionary correspond to the form fields, so you can access the author using form.cleaned_data['author']. Don’t forget to add the current post to the comment when you create it.

Note: The life cycle of submitting a form can be a little complicated, so here’s an outline of how it works:

  1. When a user visits a page containing a form, they send a GET request to the server. In this case, there’s no data entered in the form, so we just want to render the form and display it.
  2. When a user enters information and clicks the Submit button, a POST request, containing the data submitted with the form, is sent to the server. At this point, the data must be processed, and two things can happen:
    • The form is valid, and the user is redirected to the next page.
    • The form is invalid, and empty form is once again displayed. The user is back at step 1, and the process repeats.

The Django forms module will output some errors, which you can display to the user. This is beyond the scope of this tutorial, but you can read more about rendering form error messages in the Django documentation.

On line 34, save the comment and go on to add the form to the context dictionary so you can access the form in the HTML template.

The final step before you get to create the templates and actually see this blog up and running is to hook up the URLs. You’ll need create another urls.py file inside blog/ and add the URLs for the three views:

from django.urls import path from . import views  urlpatterns = [     path("", views.blog_index, name="blog_index"),     path("<int:pk>/", views.blog_detail, name="blog_detail"),     path("<category>/", views.blog_category, name="blog_category"), ] 

Once the blog-specific URLs are in place, you need to add them to the projects URL configuration using include():

from django.contrib import admin from django.urls import path, include  urlpatterns = [     path("admin/", admin.site.urls),     path("projects/", include("projects.urls")),     path("blog/", include("blog.urls")), ] 

With this set up, all the blog URLs will be prefixed with blog/, and you’ll have the following URL paths:

  • localhost:8000/blog: Blog index
  • localhost:8000/blog/1: Blog detail view of blog with pk=1
  • localhost:8000/blog/python: Blog index view of all posts with category python

These URLs won’t work just yet as you still need to create the templates.

In this section, you created all the views for your blog application. You learned how to use filters when making queries and how to create Django forms. It won’t be long now until you can see your blog app in action!

As always, don’t forget that you can check out the source code for this section on GitHub.

Blog App: Templates

The final piece of our blog app is the templates. By the end of this section, you’ll have created a fully functioning blog.

You’ll notice there are some bootstrap elements included in the templates to make the interface prettier. These aren’t the focus of the tutorial so I’ve glossed over what they do but do check out the Bootstrap docs to find out more.

The first template you’ll create is for the blog index in a new file blog/templates/blog_index.html. This will be very similar to the projects index view.

You’ll use a for loop to loop over all the posts. For each post, you’ll display the title and a snippet of the body. As always, you’ll extend the base template personal_porfolio/templates/base.html, which contains our navigation bar and some extra formatting:

 1 {% extends "base.html" %}  2 {% block page_content %}  3 <div class="col-md-8 offset-md-2">  4     <h1>Blog Index</h1>  5     <hr>  6     {% for post in posts %}  7     <h2><a href="{% url 'blog_detail' post.pk%}">{{ post.title }}</a></h2>  8     <small>  9         {{ post.created_on.date }} |&nbsp; 10         Categories:&nbsp; 11         {% for category in post.categories.all %} 12         <a href="{% url 'blog_category' category.name %}"> 13             {{ category.name }} 14         </a>&nbsp; 15         {% endfor %} 16     </small> 17     <p>{{ post.body | slice:":400" }}...</p> 18     {% endfor %} 19 </div> 20 {% endblock %} 

On line 7, we have the post title, which is a hyperlink. The link is a Django link where we are pointing to the URL named blog_detail, which takes an integer as its argument and should correspond to the pk value of the post.

Underneath the title, we’ll display the created_on attribute of the post as well as its categories. On line 11, we use another for loop to loop over all the categories assigned to the post.

On line 17, we use a template filter slice to cut off the post body at 400 characters so that the blog index is more readable.

Once that’s in place, you should be able to access this page by visiting localhost:8000/blog:

Blog Index View

Next, create another HTML file blog/templates/blog_category.html where your blog_category template will live. This should be identical to blog_index.html, except with the category name inside the h1 tag instead of Blog Index:

{% extends "base.html" %} {% block page_content %} <div class="col-md-8 offset-md-2">     <h1>{{ category | title }}</h1>     <hr>     {% for post in posts %}         <h2><a href="{% url 'blog_detail' post.pk%}">{{ post.title }}</a></h2>         <small>             {{ post.created_on.date }} |&nbsp;             Categories:&nbsp;             {% for category in post.categories.all %}             <a href="{% url 'blog_category' category.name %}">                 {{ category.name }}             </a>&nbsp;             {% endfor %}         </small>         <p>{{ post.body | slice:":400" }}...</p>     {% endfor %} </div> {% endblock %} 

Most of this template is identical to the previous template. The only difference is on line 4, where we use another Django template filter title. This applies titlecase to the string and makes words start with an uppercase character.

With that template finished, you’ll be able to access your category view. If you defined a category named python, you should be able to visit localhost:8000/blog/python and see all the posts with that category:

Blog Category View

The last template to create is the post_detail template. In this template, you’ll display the title and full body of a post.

Between the title and the body of the post, you’ll display the date the post was created and any categories. Underneath that, you’ll include a comments form so users can add a new comment. Under this, there will be a list of comments that have already been left:

 1 {% extends "base.html" %}  2 {% block page_content %}  3 <div class="col-md-8 offset-md-2">  4     <h1>{{ post.title }}</h1>  5     <small>  6         {{ post.created_on.date }} |&nbsp;  7         Categories:&nbsp;  8         {% for category in post.categories.all %}  9         <a href="{% url 'blog_category' category.name %}"> 10             {{ category.name }} 11         </a>&nbsp; 12         {% endfor %} 13     </small> 14     <p>{{ post.body | linebreaks }}</p> 15     <h3>Leave a comment:</h3> 16     <form action="/blog/{{ post.pk }}/" method="post"> 17         {% csrf_token %} 18         <div class="form-group"> 19             {{ form.author }} 20         </div> 21         <div class="form-group"> 22             {{ form.body }} 23         </div> 24         <button type="submit" class="btn btn-primary">Submit</button> 25     </form> 26     <h3>Comments:</h3> 27     {% for comment in comments %} 28     <p> 29         On {{comment.created_on.date }}&nbsp; 30         <b>{{ comment.author }}</b> wrote: 31     </p> 32     <p>{{ comment.body }}</p> 33     <hr> 34     {% endfor %} 35 </div> 36 {% endblock %} 

The first few lines of the template in which we display the post title, date, and categories is the same logic as for the previous templates. This time, when rendering the post body, use a linebreaks template filter. This tag registers line breaks as new paragraphs, so the body doesn’t appear as one long block of text.

Underneath the post, on line 16, you’ll display your form. The form action points to the URL path of the page to which you’re sending the POST request to. In this case, it’s the same as the page that is currently being visited. You then add a csrf_token, which provides security and renders the body and author fields of the form, followed by a submit button.

To get the bootstrap styling on the author and body fields, you need to add the form-control class to the text inputs.

Because Django renders the inputs for you when you include {{ form.body }} and {{ form.author }}, you can’t add these classes in the template. That’s why you added the attributes to the form widgets in the previous section.

Underneath the form, there’s another for loop that loops over all the comments on the given post. The comments, body, author, and created_on attributes are all displayed.

Once that template is in place, you should be able to visit localhost:8000/blog/1 and view your first post:

Blog Detail View

You should also be able to access the post detail pages by clicking on their title in the blog_index view.

The final finishing touch is to add a link to the blog_index to the navigation bar in base.html. This way, when you click on Blog in the navigation bar, you’ll be able to visit the blog. Check out the updates to base.html in the source code to see how to add that link.

With that now in place, your personal portfolio site is complete, and you’ve created your first Django site. The final version of the source code containing all the features can be found on GitHub, so check it out! Click around the site a bit to see all the functionality and try leaving some comments on your posts!

You may find a few things here and there that you think need polishing. Go ahead and tidy them up. The best way to learn more about this web framework is through practice, so try to extend this project and make it even better! If you’re not sure where to start, I’ve left a few ideas for you in the conclusion below!

Conclusion

Congratulations, you’ve reached the end of the tutorial! We’ve covered a lot, so make sure to keep practicing and building. The more you build the easier it will become and the less you’ll have to refer back to this article or the documentation. You’ll be building sophisticated web applications in no time.

In this tutorial you’ve seen:

  • How to create Django projects and apps
  • How to add web pages with views and templates
  • How to get user input with forms
  • How to hook your views and templates up with URL configurations
  • How to add data to your site using relational databases with Django’s Object Relational Mapper
  • How to use the Django Admin to manage your models

In addition, you’ve learned about the MVT structure of Django web applications and why Django is such a good choice for web development.

If you want to learn more about Django, do check out the documentation and make sure to stay tuned for Part 2 of this series!


[ Improve Your Python With 🐍 Python Tricks πŸ’Œ – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]

Planet Python

Jahongir Rahmonov: How to write a Python web framework. Part III.

Check out Part I here
Check out Part II here

A little reminder that this series is based on the Alcazar framework that I am writing for learning purposes. If you liked this series, show some love by starring the repo.

In the previous blog posts in the series, we started writing our own Python framework and implemented the following features:

  • WSGI compatible
  • Request Handlers
  • Routing: simple and parameterized
  • Check for duplicate routes
  • Class Based Handlers
  • Unit tests

In this part, we will add a few awesome features to the list:

  • Test Client
  • Alternative way to add routes (like Django)
  • Support for templates

Test Client

In the part 2, we wrote a couple of unit tests. However, we stopped when we needed to send HTTP requests to our handlers because we didn’t have a test client that could do that. Let’s add one then.

By far the most popular way of sending HTTP requests in Python is the Requests library by Kenneth Reitz. However, for us to be able to use it in the unit tests, we should always have our app up and running (i.e. start gunicorn before running tests). The reason is that Requests only ships with a single Transport Adapter, the HTTPAdapter. That defeats the purpose of unit tests. Unit tests should be self sustained. Fortunately for us, Sean Brant wrote a WSGI Transport Adapter for Requests that we can use to create a test client. Let’s write the code first and then discuss.

Add the following method to the main API class in api.py:

# api.py ... from requests import Session as RequestsSession from wsgiadapter import WSGIAdapter as RequestsWSGIAdapter   class API:     ...      def test_session(self, base_url="http://testserver"):         session = RequestsSession()         session.mount(prefix=base_url, adapter=RequestsWSGIAdapter(self))         return session      ... 

As written here, to use the Requests WSGI Adapter, we need to mount it to a Session object. This way, any request made using this test_session whose URL starts with the given prefix will use the given RequestsWSGIAdapter. Great, now we can use this test_session to create a test client. Create a conftest.py file and move the api fixture to this file so that it looks like this:

# conftest.py import pytest  from api import API   @pytest.fixture def api():     return API() 

In case you didn’t know, this file is where pytest looks for fixtures by default. Now, let’s create the test client fixture here:

# conftest.py ...  @pytest.fixture def client(api):     return api.test_session() 

Our client needs the api fixture and returns the test_session that we wrote earlier. Now we can use this client fixture in our unit tests. Let’s go right ahead to the test_bumbo.py file and write a unit test that tests if the client can send a request:

# test_bumbo.py ...  def test_bumbo_test_client_can_send_requests(api, client):     RESPONSE_TEXT = "THIS IS COOL"      @api.route("/hey")     def cool(req, resp):         resp.text = RESPONSE_TEXT      assert client.get("http://testserver/hey").text == RESPONSE_TEXT 

Run the unit tests by pytest test_bumbo.py and voila. We see that all the tests pass. Let’s add a couple more unit tests for the most important parts:

# test_bumbo.py ...  def test_parameterized_route(api, client):     @api.route("/{name}")     def hello(req, resp, name):         resp.text = f"hey {name}"      assert client.get(url("/matthew")).text == "hey matthew"     assert client.get(url("/ashley")).text == "hey ashley" 

This tests that the parameters that we send in the url are working.

# test_bumbo.py ...  def test_default_404_response(client):     response = client.get(url("/doesnotexist"))      assert response.status_code == 404     assert response.text == "Not found." 

This one tests that if a request is sent to a non existent route, 404(Not Found) response is returned.

The rest I will leave to you. Try to write a couple more tasks and let me know in the comments if you need any help. Here are some ideas for unit tests:

  • test that class based handlers are working with a GET request
  • test that class based handlers are working with a POST request
  • test that class based handlers are returning Method Not Allowed. response if an invalid request method is used
  • test that status code is being returned properly

Alternative way to add routes

Right now, here is how routes are added:

@api.route("/home") def handler(req, resp):     resp.text = "YOLO" 

That is, routes are added as decorators, like in Flask. Some people may like the Django way of registering urls. So, let’s give them a choice to add routes like this:

def handler(req, resp):     resp.text = "YOLO"   def handler2(req, resp):     resp.text = "YOLO2"  api.add_route("/home", handler) api.add_route("/about", handler2) 

This add_route method should do two things. Check if the route is already registered or not and if not, register it:

# api.py  class API:     ...      def add_route(self, path, handler):         assert path not in self.routes, "Such route already exists."          self.routes[path] = handler 

Pretty simple. Does this code look familiar to you? It is because we already wrote such code in the route decorator. We can now follow the DRY principle and use this add_route method inside the route decorator:

# api.py   class API:     ...      def add_route(self, path, handler):         assert path not in self.routes, "Such route already exists."          self.routes[path] = handler      def route(self, pattern):         def wrapper(handler):             self.add_route(pattern, handler)             return handler      return wrapper 

And let’s add a unit test to check if it is working:

# test_bumbo.py  def test_alternative_route(api, client):     response_text = "Alternative way to add a route"      def home(req, resp):         resp.text = response_text      api.add_route("/alternative", home)      assert client.get("http://testserver/alternative").text == response_text 

Run your tests and you will see that all of them pass.

Templates support

When I am implementing something new, I like to do something called README driven development. It is a technique where you write down how you want your API to look like before implementing. Let’s do just that for this feature. Say we have this template that we want to use in our handler:

<html>     <header>         <title>{{ title }}</title>     </header>      <body>         The name of the framework is {{ name }}     </body>  </html> 

{{ title }} and {{ name }} are variables that are sent from a handler and here is how a handler looks like:

api = API(templates_dir="templates")  @api.route("/home") def handler(req, resp):     resp.body = api.template("home.html", context={"name": "Alcazar"}) 

I want it to be as simple as possible so I just need one method that takes template name and context as params and renders that template with the given params. Also, we want templates directory to be configurable just like above.

With the API designed, we can now implement it.

For templates support, I think that Jinja2 is the best choice. It is a modern and designer-friendly templating language for Python, modelled after Django’s templates. So, if you know Django it should feel right at home.

Jinja2 uses a central object called the template Environment. We will configure this environment upon application initialization and load templates with the help of this environment. Here is how to create and configure one:

from jinja2 import Environment, FileSystemLoader  templates_env = Environment(loader=FileSystemLoader(os.path.abspath("templates"))) 

FileSystemLoader loads templates from the file system. This loader can find templates in folders on the file system and is the preferred way to load them. It takes the path to the templates directory as a parameter. Now we can use this templates_env like so:

templates_env.get_template("index.html").render({"title": "Awesome Framework", "name": "Alcazar"}) 

Now that we understand how everything works in Jinja2, let’s add it to our own framework. First, let’s install Jinja2:

pip install Jinja2 

Then, create the Environment object in the __init__ method of our API class:

# api.py from jinja2 import Environment, FileSystemLoader   class API:     def __init__(self, templates_dir="templates"):         self.routes = {}          self.templates_env = Environment(loader=FileSystemLoader(os.path.abspath(templates_dir)))      ... 

We did almost the same thing as above except that we gave templates_dir a default value of templates so that users don’t have to write it if they don’t want to. Now we have everything to implement the template method we designed earlier:

# api.py  class API:     def template(self, template_name, context=None):         if context is None:             context = {}          return self.templates_env.get_template(template_name).render(**context) 

I don’t think there is a need to explain anything here. The only thing you may wonder about is why I gave context a default value of None, checked if it is None and then set the value to an empty dictionary {}. You may say I could have given it the default value of {} in the declaration. But dict is a mutable object and it is a bad practice to set a mutable object as a default value in Python. Read more about this here.

With everything ready, we can create templates and handlers. First, create the templates folder:

mkdir templates 

Create the index.html file by doing touch templates/index.html and put the following inside:

<html>     <header>         <title>{{ title }}</title>     </header>      <body>         <h1>The name of the framework is {{ name }}</h1>     </body>  </html> 

Now we can create a handler in our app.py:

# app.py  @api.route("/template") def template_handler(req, resp):     resp.body = app.template("index.html", context={"name": "Alcazar", "title": "Best Framework"}) 

That’s it (well, almost). Start gunicorn and go to http://localhost:8000/template. You will see a big bold Internal Server Error. That’s because resp.body expects bytes and our template method returns a unicode string. Thus, we will need to encode it:

# app.py  @api.route("/template") def template_handler(req, resp):     resp.body = app.template("index.html", context={"name": "Alcazar", "title": "Best Framework"}).encode() 

Restart gunicorn and you will see our template in all its glory. In the future posts, we will remove the need to encode and make our API prettier.

Conclusion

We have implemented three new features in this post:

  • Test Client
  • Alternative way to add routes (like Django)
  • Support for templates

Make sure to let me know in the comments what other features we should implement in this series. For the next part, we will definitely add support for static files but I am not sure what other features we should add.

Check out Part I here
Check out Part II here

A little reminder that this series is based on the Alcazar framework that I am writing for learning purposes. If you liked this series, show some love by starring the repo.

That’s it for today!

Fight on!

Planet Python

Jahongir Rahmonov: How to write a Python web framework. Part II.

In the first part, we started writing our own Python framework and implemented the following features:

  • WSGI compatible
  • Request Handlers
  • Routing: simple and parameterized

Make sure to read Part I of these series before this one.

This part will be no less exciting and we will add the following features in it:

  • Check for duplicate routes
  • Class Based Handlers
  • Unit tests

Ready? Let’s get started.

Duplicate routes

Right now, our framework allows to add the same route any number of times. So, the following will work:

@app.route("/home") def home(request, response):     response.text = "Hello from the HOME page"   @app.route("/home") def home2(request, response):     response.text = "Hello from the SECOND HOME page" 

The framework will not complain and because we use a Python dictionary to store routes, only the last one will work if you go to http://localhost:8000/home/. Obviously, this is not good. We want to make sure that the framework complains if the user tries to add an existing route. As you can imagine, it is not very difficult to implement. Because we are using a Python dict to store routes, we can simply check if the given path already exists in the dictionary. If it does, we throw an exception, if it does not we let it add a route. Before we write any code, let’s remember our main API class:

# api.py  class API:     def __init__(self):         self.routes = {}      def route(self, path):         def wrapper(handler):             self.routes[path] = handler             return handler          return wrapper      def __call__(self, environ, start_response):         request = Request(environ)          response = self.handle_request(request)          return response(environ, start_response)      def find_handler(self, request_path):         for path, handler in self.routes.items():             parse_result = parse(path, request_path)             if parse_result is not None:                 return handler, parse_result.named          return None, None      def handle_request(self, request):         response = Response()          handler, kwargs = self.find_handler(request_path=request.path)          if handler is not None:             handler(request, response, **kwargs)         else:             self.default_response(response)          return response      def default_response(self, response):         response.status_code = 404         response.text = "Not found." 

We need to change the route function so that it throws an exception if an existing route is being added again:

# api.py  def route(self, path):     if path in self.routes:         throw AssertionError("Such route already exists.")      def wrapper(handler):         self.routes[path] = handler         return handler      return wrapper 

Now, trying adding the same route twice and restart your gunicorn. You should see the following exception thrown:

Traceback (most recent call last): ... AssertionError: Such route already exists. 

We can refactor it to decrease it to one line:

# api.py  def route(self, path):     assert path not in self.routes, "Such route already exists."      ... 

VoilΓ ! Onto the next feature.

Class Based Handlers

If you know Django, you know that it supports both function based and class based views (our handlers). We already have function based handlers. Now we will add class based ones which are more suitable if the handler is more complicated and bigger. Our class based handlers will look like this:

# app.py  @app.route("/book") class BooksHandler:     def get(self, req, resp):         resp.text = "Books Page"      def post(self, req, resp):         resp.text = "Endpoint to create a book"      ... 

It means that our dict where we store routes self.routes can contain both classes and functions as values. Thus, when we find a handler in the handle_request() method, we need to check if the handler is a function or if it is a class. If it is a function, it should work just like now. If it is a class, depending on the request method, we should call the appropriate method of the class. That is, if the request method is GET, we should call the get() method of the class, if it is POST we should call the post method and etc. Here is how the handle_request() method looks like now:

# api.py  def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         handler(request, response, **kwargs)     else:         self.default_response(response)      return response 

The first thing we will do is check if the found handler is a class. For that, we use the inspect module like this:

# api.py  import inspect  ...  def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         if inspect.isclass(handler):             pass   # class based handler is being used         else:             handler(request, response, **kwargs)     else:         self.default_response(response)      return response  ... 

Now, if a class based handler is being used, we need to find the appropriate method of the class depending on the request method. For that we can use the getattr built-in function:

# api.py  def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         if inspect.isclass(handler):             handler_function = getattr(handler(), request.method.lower(), None)             pass         else:             handler(request, response, **kwargs)     else:         self.default_response(response)      return response 

getattr accepts an object instance as the first param and the attribute name to get as the second. The third argument is the value to return if nothing is found. So, GET will return get, POST will return post and some_other_attribute will return None. If the handler_function is None, it means that such function was not method in the class and that this request method is not allowed:

if inspect.isclass(handler):     handler_function = getattr(handler(), request.method.lower(), None)     if handler_function is None:         raise AttributeError("Method now allowed", request.method) 

If the handler_function was actually found, then we simply call it:

if inspect.isclass(handler):     handler_function = getattr(handler(), request.method.lower(), None)     if handler_function is None:         raise AttributeError("Method now allowed", request.method)     handler_function(request, response, **kwargs) 

Now the whole method looks like this:

def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         if inspect.isclass(handler):             handler_function = getattr(handler(), request.method.lower(), None)             if handler_function is None:                 raise AttributeError("Method now allowed", request.method)             handler_function(request, response, **kwargs)         else:             handler(request, response, **kwargs)     else:         self.default_response(response) 

I don’t like that we have both handler_function and handler. We can refactor them to make it more elegant:

def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         if inspect.isclass(handler):             handler = getattr(handler(), request.method.lower(), None)             if handler is None:                 raise AttributeError("Method now allowed", request.method)          handler(request, response, **kwargs)     else:         self.default_response(response)      return response 

And that’s it. We can now test the support for class based handlers. First, if you haven’t already, add this handler to app.py:

@app.route("/book") class BooksResource:     def get(self, req, resp):         resp.text = "Books Page" 

Now, restart your gunicorn and go to the page http://localhost:8000/book and you should see the message Books Page. And there you go. We have added support for class based handlers. Play with them a little bit by implementing other methods such as post and delete as well.

Onto the next feature!

Unit Tests

What project is reliable if it has no unit tests, right? So let’s add a couple. I like using pytest, so let’s install it:

pip install pytest 

and create a file where we will write our tests:

touch test_bumbo.py 

Just to remind you, bumbo is the name of the framework. You may have named it differently. Also, if you don’t know what pytest is, I strongly recommend you look at it to understand how unit tests are written below.

First of all, let’s create a fixture for our API class that we can use in every test:

# test_bumbo.py import pytest  from api import API   @pytest.fixture def api():     return API() 

Now, for our first unit test, let’s start with something simple. Let’s test if we can add a route. If it doesn’t throw an exception, it means that the test passes successfully:

def test_basic_route(api):     @api.route("/home")     def home(req, resp):         resp.text = "YOLO" 

Run the test like this: pytest test_bumbo.py and you should see something like the following:

collected 1 item  test_bumbo.py .                                                                                                                                                            [100%]  ====== 1 passed in 0.09 seconds ====== 

Now, let’s test that it throws an exception if we try to add an existing route:

# test_bumbo.py  def test_route_overlap_throws_exception(api):     @api.route("/home")     def home(req, resp):         resp.text = "YOLO"      with pytest.raises(AssertionError):         @api.route("/home")         def home2(req, resp):             resp.text = "YOLO" 

Run the tests again and you will see that both of them pass.

We can add a lot more tests such as the default response, parameterized routing, status codes and etc. However, all of them require that we send an HTTP request to our handlers. For that we need to have a test client. But I think this post will become too big if we do it here. We will do it in the next post in these series. We will also add support for templates and a couple of other interesting stuff. So, stay tuned.

P.S. These blog posts are based on the Python web framework that I am building. So, check it out to see what is yet to come in the blog and make sure to show some love by starring the repo.

Fight on!

Planet Python