OOP Python Templating Engine.
This project is maintained by Hrabal
Hosted on GitHub Pages — Theme by orderedlist
Now it’s time to add some dynamic content to our page. We’ll get this content from the databse and we’ll use python and TemPy to put them in our template.
First we have to retrieve the data, so we use SQLAlchemy query method to get the first 10 people in our database, sorted by name. We’ll do this in our controller:
# controllers.py
from app import app, db
from models import Person
from templates.home import HomePage
@app.route('/')
def index():
people = db.session.query(Person).order_by(Person.second_name, Person.name).limit(9).all()
return HomePage(data={'people': people}).render()
We added two new imports:
db
from the app.py
modulePerson
from our modelsWe use the db.session.query
method to start a query to the db, and then we apply a sorting to our records with order_by
and a maximum number of records with limit
. all()
will run our query and so people
will contain all the extracted records.
Then we pass this data (the models.Person
instances) to our template. We can now use them as we please, when instantiating the template with the argument data={'people': people}
.
This will populate the content_data
attribute inside our template with the dict we pass to it, so we can retrieve the data while building the DOM.
Now in the HomePage
template we are going to use this data. This will be the final code of our HomePage
template:
# templates/home.py
from tempy.tags import Div, P, I, A
from .base import BasePage, CONTACTS_ICONS
class HomePage(BasePage):
def init(self):
self.body.main.container.attr(klass="album py-5 bg-light")
self.body.main.container(
Div(klass="container")(
Div(klass='row')(
persons=[
Div(klass='col-md-4')(
Div(klass='card mb-4 shadow-sm')(
A(href=f'person/{person.person_id}')(
Div(klass='card-body')(
P(klass='card-text')(
f'{person.name.title()} {person.second_name.title()}'
),
contacts=[
Div(klass='contactIcon')(
I(klass=CONTACTS_ICONS.get(contact.contact_type, 'noIcon'))
) for contact in person.contacts
]
)
)
)
) for person in self.content_data['people']
]
)
)
)
The basic Bootstrap structure consists of three nested divs with the classes container
, row
and col
inside a div with the album py-5 bg-light
css class. From the previous part of this tutorial we already have a div in which we can place our content in the self.body.main.container
attribute path so we change the css class of this empty div we created in the BasePage
template with the class we need in the home page of our app:
self.body.main.container.attr(klass="album py-5 bg-light")
We access the already created div by name, and we call the TemPy method attr
on this TemPy tag. This will translate the call arguments to attributes of the TemPy tag.
Then we add content inside this div by calling this function:
self.body.main.container(
Div(klass="container")(
Div(klass='row')(
This code will place a div with the Bootstrap’s css class “container” the body.main.container
div, and another div with the css class “row” inside it.
We’ve built the basic Boostrap Grid System skeleton, and we now have to add a variable number of cols inside it. To do so we add inside the row
a named argument called persons
which is a list comprehension:
[<some tags> for person in self.content_data['people']]
<some tags>
are the basic card structure (from the Bootstrap example) of the list we are building with a link we add so we can click on the card to go to a single person profile and a couple FontAwesome icons to indicate which kind of contacts we have for this person.
This structure would look like this in plain html:
<div class='col-md-4'>
<div class='card mb-4 shadow-sm'>
<a href='person/{id of that person}'>
<div class='card-body'>
<p>{name of the person} {second name of the person}</p>
<div class='contactIcon'><i class='{icon class of the type of this contact}'></div>
<div class='contactIcon'><i class='{icon class of the type of this contact}'></div>
</div>
</a>
</div>
</div>
This repeated for each person, with each contact structure repeated for each person’s contact. With TemPy we can translate this into a list comprehension:
[
Div(klass='col-md-4')(
Div(klass='card mb-4 shadow-sm')(
A(href=f'person/{person.person_id}')(
Div(klass='card-body')(
P(klass='card-text')(
f'{person.name.title()} {person.second_name.title()}'
),
contacts=[
Div(klass='contactIcon')(
I(klass=CONTACTS_ICONS.get(contact.contact_type, 'noIcon'))
) for contact in person.contacts
]
)
)
)
) for person in self.content_data['people']
]
As you can see we are looping over the list inside self.content_data['people']
which is the results of the query we performed in the controller. For each person in our list we are adding the basic structure with a custom link href
taken from the person.person_id
attribute we defined in the Person
model.
Inside this basic structure for each person we are adding another nested list comprehension for the person’s contact:
[
Div(klass='contactIcon')(
I(klass=CONTACTS_ICONS.get(contact.contact_type, 'noIcon'))
) for contact in person.contacts
]
CONTACT_ICONS
is a mapping we define in our templates/base.py
file, that now looks like this:
# templates/base.py
from app import app
from flask import url_for
from tempy.widgets import TempyPage
from tempy.tags import Link, Script, Meta, Main, Section, Div, H1, P
CONTACTS_ICONS = {
'email': 'fas fa-envelope',
'phone': 'fas fa-phone',
'mobile': 'fas fa-mobile',
'facebook': 'fab fa-facebook-square'
}
with app.test_request_context():
HEAD_TAGS = [
Meta(name="viewport", content="width=device-width, initial-scale=1, shrink-to-fit=no"),
Link(rel="stylesheet",
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css",
integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb",
crossorigin="anonymous"),
Link(rel="stylesheet", href=url_for('static', filename='style.css'), typ="text/css")
]
SCRIPTS = [
Script(src="https://code.jquery.com/jquery-3.2.1.min.js",
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=",
crossorigin="anonymous"),
Script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js",
integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh",
crossorigin="anonymous"),
Script(src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js",
integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ",
crossorigin="anonymous"),
Script(defer=True, src="https://use.fontawesome.com/releases/v5.0.0/js/all.js")
]
class BasePage(TempyPage):
def init(self):
self.head(HEAD_TAGS)
self.body(
main=Main(role='main')(
header=Section(klass="jumbotron text-center")(
Div(Klass="container")(
H1(klass="jumbotron-heading")('TemPy Contact Book'),
P(klass="lead text-muted")("A contact book made with TemPy."),
)
),
container=Div()
),
scripts=SCRIPTS
)
Notice that we added the mapping, but also we added a Link
inside the HEAD_TAGS
. This new link is the import of our custom css file, that we retrieve using Falsk’s url_for
method. We call url_for
inside the with app.test_request_context()
because Flask needs it to generate a valid url for our static files.
So, add this in a new file called style.css
, inside the static
folder:
.contactIcon {
padding-left: 1em;
float: right;
}
Now if we restart with python run.py
in the terminal and hit the http://localhost:5000/
we can see that the page now has a column for each person, with variable icons depending on the person’s contacts.
In the next section we’ll use another TemPy feature, TempyREPR
, that will let us define a TemPy structure directly in the models.