60: Flask Relationships - MantsSk/CA_PTUA14 GitHub Wiki

Enhancing Flask Application with Post Ability

In the previous tutorial, we fortified our Flask application with Flask-Login for user authentication and Flask-Bcrypt for secure password hashing. Now, let's take it a step further by adding the ability for users to create posts. Additionally, we'll delve a bit into Flask relationships to understand how to structure our database.

Understanding Flask Relationships

In Flask, when you're dealing with relational databases using SQLAlchemy, you'll often come across the concept of relationships between different models. Relationships define how different tables are related to each other. There are primarily three types of relationships:

One-to-Many: A single record in one table is related to multiple records in another table. Many-to-One: Multiple records in one table are related to a single record in another table. Many-to-Many: Many records in one table are related to many records in another table.

One-to-One Relationship:

In a one-to-one relationship, each record in one table is associated with exactly one record in another table.

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    profile = db.relationship('Profile', back_populates='user', uselist=False)

class Profile(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    bio = db.Column(db.Text)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship('User', back_populates='profile')

# Create a user and associated profile
user = User(username='john_doe')
profile = Profile(bio='Software engineer')
user.profile = profile

# Access user's profile
print(user.profile.bio)  # Output: 'Software engineer'

# Access profile's user
print(profile.user.username)  # Output: 'john_doe'
  • back_populates in SQLAlchemy is used to establish bidirectional relationships between models. When you define a relationship in one model, you use back_populates to specify the corresponding property in the related model. This ensures that changes made to one side of the relationship are reflected on the other side.

  • uselist is a parameter used in SQLAlchemy relationships to specify whether the relationship represents a collection of objects (True) or a single object (False). It is often used in one-to-one relationships to indicate that there should only be one related object for each record. Setting uselist=False ensures that the relationship property represents a single object, not a collection.

One-to-Many Relationship:

In a one-to-many relationship, also known as a parent-child relationship, each record in one table (the parent table) can be associated with one or more records in another table (the child table).

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    posts = db.relationship('Post', back_populates='author')

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    author = db.relationship('User', back_populates='posts')

# Create a user and associated posts
user = User(username='john_doe')
post1 = Post(title='First Post', content='Hello World!', author=user)
post2 = Post(title='Second Post', content='Another day, another post', author=user)

# Access user's posts
for post in user.posts:
    print(post.title)

# Access post's author
print(post1.author.username)  # Output: 'john_doe'
print(post2.author.username)  # Output: 'john_doe'

Defining the Parent Table (User):

  • Each user can have multiple posts, so we define a relationship property posts in the User model.
  • This property is linked to the Post model through the relationship function.
  • The back_populates parameter ensures bidirectional linkage between the User and Post models.

Defining the Child Table (Post):

  • Each post belongs to a single user, so we define a foreign key author_id in the Post model, which references the id column of the User model.
  • The author relationship property establishes a link between a post and its author, using the relationship function and back_populates parameter

Many-to-Many Relationship:

In a many-to-many relationship, each record in one table can be associated with one or more records in another table, and vice versa.

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

# Association Table for Many-to-Many Relationship
post_tag_association = db.Table('post_tag',
    db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    posts = db.relationship('Post', back_populates='author')

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    author = db.relationship('User', back_populates='posts')
    tags = db.relationship('Tag', secondary=post_tag_association, back_populates='posts')

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    posts = db.relationship('Post', secondary=post_tag_association, back_populates='tags')

# Create a post and associated tags
post = Post(title='New Post', content='Check out my new post!')
post2 = Post(title='New Post2', content='Check out my new post!')
tag1 = Tag(name='Python')
tag2 = Tag(name='Flask')
post.tags.append(tag1)
post.tags.append(tag2)
post.tags.append(tag2)
post.tags.append(tag2)
post.tags.append(tag2)

tag1.posts.append(post2)

# Access post's tags
for tag in post.tags:
    print(tag.name)

# Access tag's posts
for tag in tag1.posts:
    print(tag.title)  # Output: 'New Post'

Defining the Parent Table (User):

  • Each user can have multiple posts, so we define a relationship property posts in the User model.
  • This property is linked to the Post model through the relationship function.
  • The back_populates parameter ensures bidirectional linkage between the User and Post models.

Defining the Child Table (Post):

  • Each post belongs to a single user, so we define a foreign key author_id in the Post model, which references the id column of the User model.
  • The author relationship property establishes a link between a post and its author, using the relationship function and back_populates parameter.

Many-to-Many Relationship:

  • To represent a many-to-many relationship between Post and Tag, we create an association table post_tag_association.
  • This table contains foreign keys referencing the id columns of Post and Tag.
  • The tags relationship in the Post model and the posts relationship in the Tag model define the many-to-many relationship using the association table.
  • The back_populates parameter ensures bidirectional linkage between Post and Tag models.

Many to many example with database:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///example.db'
db = SQLAlchemy(app)

# Define models and relationships
post_tag_association = db.Table('post_tag',
    db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    posts = db.relationship('Post', back_populates='author')

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    author = db.relationship('User', back_populates='posts')
    tags = db.relationship('Tag', secondary=post_tag_association, back_populates='posts')

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    posts = db.relationship('Post', secondary=post_tag_association, back_populates='tags')

# Create all tables and add data
with app.app_context():
    db.create_all()

    # Create users
    user1 = User(username='user1')
    user2 = User(username='user2')
    db.session.add(user1)
    db.session.add(user2)

    # Create posts
    post1 = Post(title='New Post 1', content='Content of post 1', author=user1)
    post2 = Post(title='New Post 2', content='Content of post 2', author=user2)
    db.session.add(post1)
    db.session.add(post2)

    # Create tags
    tag1 = Tag(name='Tag 1')
    tag2 = Tag(name='Tag 2')
    db.session.add(tag1)
    db.session.add(tag2)

    # Associate tags with posts
    post1.tags.append(tag1)
    post1.tags.append(tag2)
    post2.tags.append(tag2)

    # Commit changes to the database
    db.session.commit()

Improving our application with Post ability

Lets continue with our application that we made in past lectures - https://github.com/MantsSk/CA_PTUA10/tree/master/59Pamoka%20-%20FlaskLoginRegistartionImprovement/end_code

First, let's modify our database schema to accommodate posts. We'll create a new model for posts and establish a one-to-many relationship between the User and Post models.

Post model

from sqlalchemy.orm import relationship

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.datetime.now())
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    author = relationship('User', back_populates='posts')
  • user_id column establishes a foreign key relationship with the id column of the User model.
  • author establishes a relationship back to the User model, indicating that each post is associated with a single user.

User model

In the User model, we add a new field posts to represent the posts created by the user.

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    password = db.Column(db.String(60), nullable=False)
    posts = relationship('Post', back_populates='author')

Modifying Post Creation and Display

Now, let's update our routes and templates to allow users to create posts and view them.

@app.route('/post/new', methods=['GET', 'POST'])
@login_required
def new_post():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']
        post = Post(title=title, content=content, author=current_user)
        db.session.add(post)
        db.session.commit()
        return redirect(url_for('home'))
    return render_template('create_post.html', title='New Post')

@app.route('/post/<int:post_id>')
def post(post_id):
    post = Post.query.get_or_404(post_id)
    return render_template('post.html', title=post.title, post=post)

In the new_post route:

  • We check if the request method is POST.
  • If it is, we extract the title and content from the form and create a new Post object associated with the current user.

In the post route:

  • We retrieve the post with the given ID from the database and render it using the post.html template.

###Template Modification

We also need to modify our templates to include forms for creating posts and to display posts.

Creating create_post.html template:

{% extends "base.html" %}

{% block title %}Create post{% endblock %}

{% block content %}
    <div class="content-section">
        <form method="POST">
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Create Post</legend>
                <div class="form-group">
                    <label for="title" class="form-control-label">Title</label>
                    <input type="text" id="title" name="title" class="form-control form-control-lg" required>
                </div>
                <div class="form-group">
                    <label for="content" class="form-control-label">Content</label>
                    <textarea id="content" name="content" class="form-control form-control-lg" required></textarea>
                </div>
            </fieldset>
            <div class="form-group">
                <button type="submit" class="btn btn-outline-info">Submit</button>
            </div>
        </form>
    </div>
{% endblock %}

Creating post.html template

{% extends "base.html" %}
{% block content %}
    <article class="media content-section">
        <div class="media-body">
            <div class="article-metadata">
                <a class="mr-2" href="#">{{ post.author.username }}</a>
                <small class="text-muted">{{ post.date_posted.strftime('%Y-%m-%d') }}</small>
            </div>
            <h2 class="article-title">{{ post.title }}</h2>
            <p class="article-content">{{ post.content }}</p>
        </div>
    </article>
{% endblock %}

Now by going to http://127.0.0.1:5000/post/new we can create a new post and by going to http://127.0.0.1:5000/post/1, we can look at our first created post!

Adding add post to navbar

<div class="navbar">
    <a class="{% if active == 'home' %}active{% endif %}" href="{{ url_for('home') }}">Home</a>
    {% if current_user.is_authenticated %}
        <a href="{{ url_for('new_post') }}">Add post</a>
        <a href="{{ url_for('logout') }}">Logout</a>
        <span style="float:right; color:black; padding-right: 20px;">Hello, {{ current_user.username }}!</span>
    {% else %}
        <a href="{{ url_for('register') }}">Register</a>
        <a href="{{ url_for('login') }}">Login</a>
    {% endif %}
</div>

Displaying posts in main page

The last thing we can do today is show all the posts in the main page, we can do this by modifying our home route and index.html.

Our home route should include query to gather all posts:

@app.route('/')
def home():
    posts = Post.query.all()
    return render_template('index.html', posts=posts)

And in template, we should print all the posts. Something like this:

{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
    <div class="content-section">
        <h2>All Posts</h2>
        <ul>
            {% for post in posts %}
                <li>
                    <h3>{{ post.title }}</h3>
                    <p>{{ post.content }}</p>
                    <p>Posted on: {{ post.date_posted.strftime('%Y-%m-%d') }}</p>
                    <p>Posted by: {{ post.author.username }} </p>

                </li>
            {% endfor %}
        </ul>
    </div>
{% endblock %}

And now you should be able to see all the posts visible in the home page :) You could modify the query to show only the user posts and etc, by filtering and etc, but for now, we are going to leave all user posts visible in main page.

⚠️ **GitHub.com Fallback** ⚠️