Flask Weekend Project

Building a Complete Web Application with Database Integration and API

Introduction

Congratulations on completing Module 17 on Flask! This weekend project is designed to help you consolidate everything you've learned by building a complete web application with database integration and a RESTful API. As you work through this project, you'll practice all the key concepts from the module, from template rendering to database operations to creating a structured API.

For this project, we'll be using George Polya's 4-step problem-solving procedure:

  1. Understand the problem - Clarify what we're trying to build
  2. Devise a plan - Design the solution step-by-step
  3. Carry out the plan - Implement the solution
  4. Look back - Evaluate, test, and refine the solution

This approach will help ensure that you don't get overwhelmed by the complexity of building a full application and will give you a systematic way to tackle the challenge.

graph LR A[Understand the Problem] --> B[Devise a Plan] B --> C[Carry Out the Plan] C --> D[Look Back] D -->|Refine| B style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#bfb,stroke:#333,stroke-width:2px style D fill:#fbf,stroke:#333,stroke-width:2px

1. Understand the Problem

Let's define what we're building: a personal library management system. This application will allow users to keep track of their books, manage reading lists, rate and review books, and search for new titles.

Project Requirements

Clarifying Questions

Following George Polya's method, let's clarify the problem:

  1. What data do we need to store?

    We need to store information about users, books, reading lists, reviews, and relationships between them.

  2. What functionality does the web interface need?

    Users need to be able to browse, search, and manage their books and reading lists through a user-friendly interface.

  3. What should the API provide?

    The API should provide access to book data, including CRUD operations for books, reading lists, and reviews.

  4. What are our technical constraints?

    We'll use Flask, SQLAlchemy, Flask-RESTful/Marshmallow, and other tools we've learned in the module.

Technologies We'll Use

2. Devise a Plan

Now that we understand what we're building, let's break down the project into manageable steps:

Step 1: Set Up Project Structure

Step 2: Design Database Models

Step 3: Implement Authentication

Step 4: Develop Book Management

Step 5: Implement Reading Lists

Step 6: Add Reviews and Ratings

Step 7: Design and Implement API

Step 8: Styling and UI Improvements

Step 9: Testing and Debugging

Step 10: Deployment (Optional)

Project Timeline

This is a substantial project, so let's allocate time realistically:

3. Carry Out the Plan

Now let's implement our plan step by step, providing code and explanations for each part:

Step 1: Set Up Project Structure

First, let's create our project directory and set up the basic structure:

# Create project directory
$ mkdir bookshelf
$ cd bookshelf

# Create virtual environment
$ python -m venv venv
$ source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install required packages
$ pip install flask flask-sqlalchemy flask-migrate flask-login flask-wtf flask-restful flask-marshmallow marshmallow-sqlalchemy python-dotenv email-validator

Now, let's create our basic project structure:

bookshelf/
├── app/
│   ├── __init__.py
│   ├── config.py
│   ├── extensions.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── book.py
│   │   ├── reading_list.py
│   │   └── review.py
│   ├── blueprints/
│   │   ├── __init__.py
│   │   ├── auth/
│   │   │   ├── __init__.py
│   │   │   ├── routes.py
│   │   │   └── forms.py
│   │   ├── main/
│   │   │   ├── __init__.py
│   │   │   └── routes.py
│   │   ├── books/
│   │   │   ├── __init__.py
│   │   │   ├── routes.py
│   │   │   └── forms.py
│   │   ├── reading_lists/
│   │   │   ├── __init__.py
│   │   │   ├── routes.py
│   │   │   └── forms.py
│   │   └── api/
│   │       ├── __init__.py
│   │       ├── resources.py
│   │       └── schemas.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── auth/
│   │   ├── main/
│   │   ├── books/
│   │   └── reading_lists/
│   └── static/
│       ├── css/
│       └── js/
├── migrations/
├── .env
└── run.py

Now, let's implement the basic Flask application:

# app/config.py
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///bookshelf.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False


# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
login.login_message = 'Please log in to access this page.'
ma = Marshmallow()


# app/__init__.py
from flask import Flask
from app.config import Config
from app.extensions import db, migrate, login, ma

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)
    
    # Initialize extensions
    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)
    ma.init_app(app)
    
    # Register blueprints
    from app.blueprints.auth import auth
    from app.blueprints.main import main
    from app.blueprints.books import books
    from app.blueprints.reading_lists import reading_lists
    from app.blueprints.api import api_bp
    
    app.register_blueprint(auth)
    app.register_blueprint(main)
    app.register_blueprint(books)
    app.register_blueprint(reading_lists)
    app.register_blueprint(api_bp, url_prefix='/api')
    
    return app


# app/models/__init__.py
from app.models.user import User
from app.models.book import Book
from app.models.reading_list import ReadingList, reading_list_books
from app.models.review import Review


# run.py
from app import create_app, db
from app.models import User, Book, ReadingList, Review

app = create_app()

@app.shell_context_processor
def make_shell_context():
    return {
        'db': db,
        'User': User,
        'Book': Book,
        'ReadingList': ReadingList,
        'Review': Review
    }

if __name__ == '__main__':
    app.run(debug=True)

Step 2: Design Database Models

Now let's define our database models:

# app/models/user.py
from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app.extensions import db, login

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, nullable=False, index=True)
    email = db.Column(db.String(120), unique=True, nullable=False, index=True)
    password_hash = db.Column(db.String(128), nullable=False)
    joined_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # Relationships
    books = db.relationship('Book', backref='owner', lazy='dynamic', cascade='all, delete-orphan')
    reading_lists = db.relationship('ReadingList', backref='owner', lazy='dynamic', cascade='all, delete-orphan')
    reviews = db.relationship('Review', backref='author', lazy='dynamic', cascade='all, delete-orphan')
    
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
        
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def __repr__(self):
        return f''

@login.user_loader
def load_user(id):
    return User.query.get(int(id))


# app/models/book.py
from datetime import datetime
from app.extensions import db

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False, index=True)
    author = db.Column(db.String(100), nullable=False, index=True)
    isbn = db.Column(db.String(20), index=True)
    publisher = db.Column(db.String(100))
    publication_year = db.Column(db.Integer)
    description = db.Column(db.Text)
    genre = db.Column(db.String(50), index=True)
    cover_image = db.Column(db.String(200))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # Foreign keys
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    # Relationships
    reviews = db.relationship('Review', backref='book', lazy='dynamic', cascade='all, delete-orphan')
    
    def __repr__(self):
        return f''
    
    @property
    def average_rating(self):
        reviews = self.reviews.all()
        if not reviews:
            return 0
        return sum(review.rating for review in reviews) / len(reviews)


# app/models/reading_list.py
from datetime import datetime
from app.extensions import db

# Association table for the many-to-many relationship
reading_list_books = db.Table('reading_list_books',
    db.Column('reading_list_id', db.Integer, db.ForeignKey('reading_list.id'), primary_key=True),
    db.Column('book_id', db.Integer, db.ForeignKey('book.id'), primary_key=True),
    db.Column('added_at', db.DateTime, default=datetime.utcnow)
)

class ReadingList(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # Foreign keys
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    # Relationships
    books = db.relationship('Book', secondary=reading_list_books, lazy='dynamic',
                          backref=db.backref('reading_lists', lazy='dynamic'))
    
    def __repr__(self):
        return f''


# app/models/review.py
from datetime import datetime
from app.extensions import db

class Review(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    rating = db.Column(db.Integer, nullable=False)  # 1-5 stars
    content = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Foreign keys
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    book_id = db.Column(db.Integer, db.ForeignKey('book.id'), nullable=False)
    
    # Ensure a user can only review a book once
    __table_args__ = (
        db.UniqueConstraint('user_id', 'book_id', name='uix_user_book_review'),
    )
    
    def __repr__(self):
        return f''

Step 3: Implement Authentication

Let's implement user authentication:

# app/blueprints/auth/__init__.py
from flask import Blueprint

auth = Blueprint('auth', __name__, url_prefix='/auth')

from app.blueprints.auth import routes


# app/blueprints/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
from app.models import User

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), Length(min=3, max=64)])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
    password2 = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')
    
    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('Please use a different username.')
    
    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')


# app/blueprints/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user, login_required
from app.extensions import db
from app.blueprints.auth import auth
from app.blueprints.auth.forms import LoginForm, RegistrationForm
from app.models import User

@auth.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!', 'success')
        return redirect(url_for('auth.login'))
    
    return render_template('auth/register.html', title='Register', form=form)

@auth.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.index'))
    
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password', 'danger')
            return redirect(url_for('auth.login'))
        
        login_user(user, remember=form.remember_me.data)
        
        # Redirect to requested page or default to home page
        next_page = request.args.get('next')
        if not next_page or not next_page.startswith('/'):
            next_page = url_for('main.index')
        
        return redirect(next_page)
    
    return render_template('auth/login.html', title='Sign In', form=form)

@auth.route('/logout')
def logout():
    logout_user()
    flash('You have been logged out.', 'info')
    return redirect(url_for('main.index'))

@auth.route('/profile')
@login_required
def profile():
    return render_template('auth/profile.html', title='Profile')

Step 4-6: Implement Main Features

Now let's implement the main features of our application, including book management, reading lists, and reviews:

# app/blueprints/main/__init__.py
from flask import Blueprint

main = Blueprint('main', __name__)

from app.blueprints.main import routes


# app/blueprints/main/routes.py
from flask import render_template, redirect, url_for
from app.blueprints.main import main
from app.models import Book, ReadingList, Review

@main.route('/')
def index():
    recent_books = Book.query.order_by(Book.created_at.desc()).limit(5).all()
    top_rated_books = Book.query.join(Review).group_by(Book.id).order_by(db.func.avg(Review.rating).desc()).limit(5).all()
    
    return render_template('main/index.html', title='Home', 
                          recent_books=recent_books, top_rated_books=top_rated_books)

@main.route('/about')
def about():
    return render_template('main/about.html', title='About')


# app/blueprints/books/__init__.py
from flask import Blueprint

books = Blueprint('books', __name__, url_prefix='/books')

from app.blueprints.books import routes


# app/blueprints/books/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, IntegerField, SelectField, SubmitField
from wtforms.validators import DataRequired, Length, Optional, NumberRange, URL

class BookForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired(), Length(max=200)])
    author = StringField('Author', validators=[DataRequired(), Length(max=100)])
    isbn = StringField('ISBN', validators=[Optional(), Length(max=20)])
    publisher = StringField('Publisher', validators=[Optional(), Length(max=100)])
    publication_year = IntegerField('Publication Year', validators=[Optional(), NumberRange(min=1000, max=2100)])
    description = TextAreaField('Description', validators=[Optional()])
    genre = StringField('Genre', validators=[Optional(), Length(max=50)])
    cover_image = StringField('Cover Image URL', validators=[Optional(), URL()])
    submit = SubmitField('Save Book')

class SearchForm(FlaskForm):
    query = StringField('Search', validators=[DataRequired()])
    category = SelectField('Category', choices=[
        ('title', 'Title'),
        ('author', 'Author'),
        ('genre', 'Genre'),
        ('isbn', 'ISBN')
    ])
    submit = SubmitField('Search')

class ReviewForm(FlaskForm):
    rating = SelectField('Rating', choices=[(1, '1 Star'), (2, '2 Stars'), (3, '3 Stars'), 
                                         (4, '4 Stars'), (5, '5 Stars')], coerce=int)
    content = TextAreaField('Review', validators=[Optional(), Length(max=1000)])
    submit = SubmitField('Submit Review')


# app/blueprints/books/routes.py
from flask import render_template, redirect, url_for, flash, request, abort
from flask_login import current_user, login_required
from app.extensions import db
from app.blueprints.books import books
from app.blueprints.books.forms import BookForm, SearchForm, ReviewForm
from app.models import Book, Review

@books.route('/')
def index():
    page = request.args.get('page', 1, type=int)
    books_list = Book.query.order_by(Book.title).paginate(page=page, per_page=12)
    return render_template('books/index.html', title='All Books', books=books_list)

@books.route('/create', methods=['GET', 'POST'])
@login_required
def create():
    form = BookForm()
    if form.validate_on_submit():
        book = Book(
            title=form.title.data,
            author=form.author.data,
            isbn=form.isbn.data,
            publisher=form.publisher.data,
            publication_year=form.publication_year.data,
            description=form.description.data,
            genre=form.genre.data,
            cover_image=form.cover_image.data,
            user_id=current_user.id
        )
        db.session.add(book)
        db.session.commit()
        flash('Your book has been added!', 'success')
        return redirect(url_for('books.detail', id=book.id))
    
    return render_template('books/create.html', title='Add Book', form=form)

@books.route('/')
def detail(id):
    book = Book.query.get_or_404(id)
    review_form = ReviewForm()
    user_review = None
    
    if current_user.is_authenticated:
        user_review = Review.query.filter_by(user_id=current_user.id, book_id=book.id).first()
    
    return render_template('books/detail.html', title=book.title, book=book, 
                          review_form=review_form, user_review=user_review)

@books.route('//edit', methods=['GET', 'POST'])
@login_required
def edit(id):
    book = Book.query.get_or_404(id)
    
    # Check if the current user is the owner of the book
    if book.user_id != current_user.id:
        abort(403)
    
    form = BookForm()
    
    if form.validate_on_submit():
        book.title = form.title.data
        book.author = form.author.data
        book.isbn = form.isbn.data
        book.publisher = form.publisher.data
        book.publication_year = form.publication_year.data
        book.description = form.description.data
        book.genre = form.genre.data
        book.cover_image = form.cover_image.data
        
        db.session.commit()
        flash('Your book has been updated!', 'success')
        return redirect(url_for('books.detail', id=book.id))
    
    elif request.method == 'GET':
        form.title.data = book.title
        form.author.data = book.author
        form.isbn.data = book.isbn
        form.publisher.data = book.publisher
        form.publication_year.data = book.publication_year
        form.description.data = book.description
        form.genre.data = book.genre
        form.cover_image.data = book.cover_image
    
    return render_template('books/edit.html', title='Edit Book', form=form, book=book)

@books.route('//delete', methods=['POST'])
@login_required
def delete(id):
    book = Book.query.get_or_404(id)
    
    # Check if the current user is the owner of the book
    if book.user_id != current_user.id:
        abort(403)
    
    db.session.delete(book)
    db.session.commit()
    flash('Your book has been deleted!', 'success')
    return redirect(url_for('books.index'))

@books.route('/search', methods=['GET', 'POST'])
def search():
    form = SearchForm()
    results = []
    
    if form.validate_on_submit() or request.args.get('query'):
        query = form.query.data if form.validate_on_submit() else request.args.get('query')
        category = form.category.data if form.validate_on_submit() else request.args.get('category', 'title')
        
        if category == 'title':
            results = Book.query.filter(Book.title.ilike(f'%{query}%')).all()
        elif category == 'author':
            results = Book.query.filter(Book.author.ilike(f'%{query}%')).all()
        elif category == 'genre':
            results = Book.query.filter(Book.genre.ilike(f'%{query}%')).all()
        elif category == 'isbn':
            results = Book.query.filter(Book.isbn.ilike(f'%{query}%')).all()
    
    return render_template('books/search.html', title='Search Books', form=form, results=results)

@books.route('//review', methods=['POST'])
@login_required
def add_review(id):
    book = Book.query.get_or_404(id)
    form = ReviewForm()
    
    if form.validate_on_submit():
        # Check if user already has a review for this book
        existing_review = Review.query.filter_by(user_id=current_user.id, book_id=book.id).first()
        
        if existing_review:
            # Update existing review
            existing_review.rating = form.rating.data
            existing_review.content = form.content.data
            flash('Your review has been updated!', 'success')
        else:
            # Create new review
            review = Review(
                rating=form.rating.data,
                content=form.content.data,
                user_id=current_user.id,
                book_id=book.id
            )
            db.session.add(review)
            flash('Your review has been added!', 'success')
        
        db.session.commit()
    
    return redirect(url_for('books.detail', id=book.id))


# app/blueprints/reading_lists/__init__.py
from flask import Blueprint

reading_lists = Blueprint('reading_lists', __name__, url_prefix='/reading-lists')

from app.blueprints.reading_lists import routes


# app/blueprints/reading_lists/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length, Optional

class ReadingListForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired(), Length(max=100)])
    description = TextAreaField('Description', validators=[Optional(), Length(max=500)])
    submit = SubmitField('Save')


# app/blueprints/reading_lists/routes.py
from flask import render_template, redirect, url_for, flash, request, abort
from flask_login import current_user, login_required
from app.extensions import db
from app.blueprints.reading_lists import reading_lists
from app.blueprints.reading_lists.forms import ReadingListForm
from app.models import ReadingList, Book

@reading_lists.route('/')
@login_required
def index():
    lists = ReadingList.query.filter_by(user_id=current_user.id).all()
    return render_template('reading_lists/index.html', title='My Reading Lists', reading_lists=lists)

@reading_lists.route('/create', methods=['GET', 'POST'])
@login_required
def create():
    form = ReadingListForm()
    
    if form.validate_on_submit():
        reading_list = ReadingList(
            name=form.name.data,
            description=form.description.data,
            user_id=current_user.id
        )
        db.session.add(reading_list)
        db.session.commit()
        flash('Your reading list has been created!', 'success')
        return redirect(url_for('reading_lists.detail', id=reading_list.id))
    
    return render_template('reading_lists/create.html', title='Create Reading List', form=form)

@reading_lists.route('/')
@login_required
def detail(id):
    reading_list = ReadingList.query.get_or_404(id)
    
    # Ensure the current user is the owner of the reading list
    if reading_list.user_id != current_user.id:
        abort(403)
    
    return render_template('reading_lists/detail.html', title=reading_list.name, reading_list=reading_list)

@reading_lists.route('//edit', methods=['GET', 'POST'])
@login_required
def edit(id):
    reading_list = ReadingList.query.get_or_404(id)
    
    # Ensure the current user is the owner of the reading list
    if reading_list.user_id != current_user.id:
        abort(403)
    
    form = ReadingListForm()
    
    if form.validate_on_submit():
        reading_list.name = form.name.data
        reading_list.description = form.description.data
        db.session.commit()
        flash('Your reading list has been updated!', 'success')
        return redirect(url_for('reading_lists.detail', id=reading_list.id))
    
    elif request.method == 'GET':
        form.name.data = reading_list.name
        form.description.data = reading_list.description
    
    return render_template('reading_lists/edit.html', title='Edit Reading List', form=form, reading_list=reading_list)

@reading_lists.route('//delete', methods=['POST'])
@login_required
def delete(id):
    reading_list = ReadingList.query.get_or_404(id)
    
    # Ensure the current user is the owner of the reading list
    if reading_list.user_id != current_user.id:
        abort(403)
    
    db.session.delete(reading_list)
    db.session.commit()
    flash('Your reading list has been deleted!', 'success')
    return redirect(url_for('reading_lists.index'))

@reading_lists.route('//add/', methods=['POST'])
@login_required
def add_book(list_id, book_id):
    reading_list = ReadingList.query.get_or_404(list_id)
    book = Book.query.get_or_404(book_id)
    
    # Ensure the current user is the owner of the reading list
    if reading_list.user_id != current_user.id:
        abort(403)
    
    # Check if the book is already in the reading list
    if book in reading_list.books:
        flash('This book is already in the reading list.', 'info')
    else:
        reading_list.books.append(book)
        db.session.commit()
        flash('Book added to the reading list!', 'success')
    
    return redirect(url_for('reading_lists.detail', id=reading_list.id))

@reading_lists.route('//remove/', methods=['POST'])
@login_required
def remove_book(list_id, book_id):
    reading_list = ReadingList.query.get_or_404(list_id)
    book = Book.query.get_or_404(book_id)
    
    # Ensure the current user is the owner of the reading list
    if reading_list.user_id != current_user.id:
        abort(403)
    
    # Check if the book is in the reading list
    if book in reading_list.books:
        reading_list.books.remove(book)
        db.session.commit()
        flash('Book removed from the reading list.', 'success')
    else:
        flash('This book is not in the reading list.', 'info')
    
    return redirect(url_for('reading_lists.detail', id=reading_list.id))

Step 7: Design and Implement API

Now let's implement our RESTful API:

# app/blueprints/api/__init__.py
from flask import Blueprint

api_bp = Blueprint('api', __name__)

from app.blueprints.api import resources


# app/blueprints/api/schemas.py
from app.extensions import ma
from app.models import User, Book, ReadingList, Review
from marshmallow import fields, validates, ValidationError

class UserSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = User
        exclude = ('password_hash',)
        load_instance = True

class ReviewSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = Review
        include_fk = True
        load_instance = True
    
    author = fields.Nested('UserSchema', only=('id', 'username'))

class BookSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = Book
        include_fk = True
        load_instance = True
    
    reviews = fields.Nested(ReviewSchema, many=True, exclude=('book',))
    owner = fields.Nested('UserSchema', only=('id', 'username'))
    average_rating = fields.Float()
    
    @validates('publication_year')
    def validate_year(self, value):
        if value and (value < 1000 or value > 2100):
            raise ValidationError('Publication year must be between 1000 and 2100')

class ReadingListSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = ReadingList
        include_fk = True
        load_instance = True
    
    books = fields.Nested(BookSchema, many=True, exclude=('reviews', 'reading_lists'))
    owner = fields.Nested('UserSchema', only=('id', 'username'))

# Create schema instances
user_schema = UserSchema()
users_schema = UserSchema(many=True)
book_schema = BookSchema()
books_schema = BookSchema(many=True)
review_schema = ReviewSchema()
reviews_schema = ReviewSchema(many=True)
reading_list_schema = ReadingListSchema()
reading_lists_schema = ReadingListSchema(many=True)


# app/blueprints/api/resources.py
from flask import request, jsonify, g
from flask_restful import Api, Resource, abort
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from app.extensions import db
from app.blueprints.api import api_bp
from app.blueprints.api.schemas import (
    user_schema, users_schema, book_schema, books_schema,
    review_schema, reviews_schema, reading_list_schema, reading_lists_schema
)
from app.models import User, Book, ReadingList, Review
from marshmallow import ValidationError

# Set up authentication
basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth()

@basic_auth.verify_password
def verify_password(username, password):
    user = User.query.filter_by(username=username).first()
    if user and user.check_password(password):
        g.user = user
        return user

@token_auth.verify_token
def verify_token(token):
    # In a real app, you would implement token verification here
    # For simplicity, we'll use basic auth for this example
    return None

# Create the API
api = Api(api_bp)

# Resource classes
class BookListAPI(Resource):
    def get(self):
        # Get query parameters for filtering
        author = request.args.get('author')
        genre = request.args.get('genre')
        
        # Start with base query
        query = Book.query
        
        # Apply filters
        if author:
            query = query.filter(Book.author.ilike(f'%{author}%'))
        if genre:
            query = query.filter(Book.genre.ilike(f'%{genre}%'))
        
        # Execute query
        books = query.all()
        
        return books_schema.dump(books)
    
    @basic_auth.login_required
    def post(self):
        try:
            # Get JSON data
            json_data = request.get_json()
            
            # Ensure user_id is set to current user
            json_data['user_id'] = g.user.id
            
            # Validate and deserialize input
            book = book_schema.load(json_data)
            
            # Save the book
            db.session.add(book)
            db.session.commit()
            
            return book_schema.dump(book), 201
        
        except ValidationError as err:
            return {'errors': err.messages}, 400

class BookAPI(Resource):
    def get(self, id):
        book = Book.query.get_or_404(id)
        return book_schema.dump(book)
    
    @basic_auth.login_required
    def put(self, id):
        book = Book.query.get_or_404(id)
        
        # Check if the current user is the owner
        if book.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        try:
            # Get JSON data
            json_data = request.get_json()
            
            # Validate and deserialize input
            book = book_schema.load(json_data, instance=book, partial=True)
            
            # Save the book
            db.session.commit()
            
            return book_schema.dump(book)
        
        except ValidationError as err:
            return {'errors': err.messages}, 400
    
    @basic_auth.login_required
    def delete(self, id):
        book = Book.query.get_or_404(id)
        
        # Check if the current user is the owner
        if book.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        db.session.delete(book)
        db.session.commit()
        
        return '', 204

class ReadingListListAPI(Resource):
    @basic_auth.login_required
    def get(self):
        reading_lists = ReadingList.query.filter_by(user_id=g.user.id).all()
        return reading_lists_schema.dump(reading_lists)
    
    @basic_auth.login_required
    def post(self):
        try:
            # Get JSON data
            json_data = request.get_json()
            
            # Ensure user_id is set to current user
            json_data['user_id'] = g.user.id
            
            # Validate and deserialize input
            reading_list = reading_list_schema.load(json_data)
            
            # Save the reading list
            db.session.add(reading_list)
            db.session.commit()
            
            return reading_list_schema.dump(reading_list), 201
        
        except ValidationError as err:
            return {'errors': err.messages}, 400

class ReadingListAPI(Resource):
    @basic_auth.login_required
    def get(self, id):
        reading_list = ReadingList.query.get_or_404(id)
        
        # Check if the current user is the owner
        if reading_list.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        return reading_list_schema.dump(reading_list)
    
    @basic_auth.login_required
    def put(self, id):
        reading_list = ReadingList.query.get_or_404(id)
        
        # Check if the current user is the owner
        if reading_list.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        try:
            # Get JSON data
            json_data = request.get_json()
            
            # Validate and deserialize input
            reading_list = reading_list_schema.load(json_data, instance=reading_list, partial=True)
            
            # Save the reading list
            db.session.commit()
            
            return reading_list_schema.dump(reading_list)
        
        except ValidationError as err:
            return {'errors': err.messages}, 400
    
    @basic_auth.login_required
    def delete(self, id):
        reading_list = ReadingList.query.get_or_404(id)
        
        # Check if the current user is the owner
        if reading_list.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        db.session.delete(reading_list)
        db.session.commit()
        
        return '', 204

class ReadingListBookAPI(Resource):
    @basic_auth.login_required
    def post(self, list_id, book_id):
        reading_list = ReadingList.query.get_or_404(list_id)
        book = Book.query.get_or_404(book_id)
        
        # Check if the current user is the owner of the reading list
        if reading_list.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        # Check if the book is already in the reading list
        if book in reading_list.books:
            return {'message': 'Book is already in this reading list'}, 400
        
        # Add book to reading list
        reading_list.books.append(book)
        db.session.commit()
        
        return {'message': 'Book added to reading list'}, 201
    
    @basic_auth.login_required
    def delete(self, list_id, book_id):
        reading_list = ReadingList.query.get_or_404(list_id)
        book = Book.query.get_or_404(book_id)
        
        # Check if the current user is the owner of the reading list
        if reading_list.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        # Check if the book is in the reading list
        if book not in reading_list.books:
            return {'message': 'Book is not in this reading list'}, 400
        
        # Remove book from reading list
        reading_list.books.remove(book)
        db.session.commit()
        
        return '', 204

class ReviewListAPI(Resource):
    def get(self, book_id):
        book = Book.query.get_or_404(book_id)
        reviews = book.reviews.all()
        return reviews_schema.dump(reviews)
    
    @basic_auth.login_required
    def post(self, book_id):
        book = Book.query.get_or_404(book_id)
        
        try:
            # Get JSON data
            json_data = request.get_json()
            
            # Ensure user_id and book_id are set
            json_data['user_id'] = g.user.id
            json_data['book_id'] = book_id
            
            # Check if the user already has a review for this book
            existing_review = Review.query.filter_by(user_id=g.user.id, book_id=book_id).first()
            
            if existing_review:
                # Update existing review
                review = review_schema.load(json_data, instance=existing_review, partial=True)
                message = 'Review updated'
            else:
                # Create new review
                review = review_schema.load(json_data)
                db.session.add(review)
                message = 'Review created'
            
            db.session.commit()
            
            return {'message': message, 'review': review_schema.dump(review)}, 201
        
        except ValidationError as err:
            return {'errors': err.messages}, 400

class ReviewAPI(Resource):
    def get(self, id):
        review = Review.query.get_or_404(id)
        return review_schema.dump(review)
    
    @basic_auth.login_required
    def put(self, id):
        review = Review.query.get_or_404(id)
        
        # Check if the current user is the author
        if review.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        try:
            # Get JSON data
            json_data = request.get_json()
            
            # Validate and deserialize input
            review = review_schema.load(json_data, instance=review, partial=True)
            
            # Save the review
            db.session.commit()
            
            return review_schema.dump(review)
        
        except ValidationError as err:
            return {'errors': err.messages}, 400
    
    @basic_auth.login_required
    def delete(self, id):
        review = Review.query.get_or_404(id)
        
        # Check if the current user is the author
        if review.user_id != g.user.id:
            abort(403, message="Permission denied")
        
        db.session.delete(review)
        db.session.commit()
        
        return '', 204

# Register resources
api.add_resource(BookListAPI, '/books')
api.add_resource(BookAPI, '/books/')
api.add_resource(ReadingListListAPI, '/reading-lists')
api.add_resource(ReadingListAPI, '/reading-lists/')
api.add_resource(ReadingListBookAPI, '/reading-lists//books/')
api.add_resource(ReviewListAPI, '/books//reviews')
api.add_resource(ReviewAPI, '/reviews/')

Step 8: Templates and Styling

Let's create some basic templates for our application:





    
    
    {% block title %}Bookshelf{% endblock %}
    
    
    
    
    {% block styles %}{% endblock %}


    
    

    
    
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
{% block content %}{% endblock %}

© 2025 Bookshelf - A Flask Weekend Project

{% block scripts %}{% endblock %} {% extends 'base.html' %} {% block content %}

Welcome to Bookshelf

Your personal library management system

{% if not current_user.is_authenticated %}

Join today to track your books, create reading lists, and more!

Sign Up Now {% endif %}

Recently Added Books

{% for book in recent_books %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}
{{ book.title }}
{{ book.author }}

{{ book.description|truncate(100) }}

View Details
{% endfor %}

Top Rated Books

{% for book in top_rated_books %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}
{{ book.title }}
{{ book.author }}
{% for i in range(5) %} {% if i < book.average_rating|int %} {% elif i < book.average_rating|round(0, 'ceil')|int and (book.average_rating % 1) > 0 %} {% else %} {% endif %} {% endfor %} ({{ book.average_rating|round(1) }})
View Details
{% endfor %}
{% endblock %} {% extends 'base.html' %} {% block content %}

Sign In

{{ form.hidden_tag() }}
{{ form.username.label(class='form-label') }} {{ form.username(class='form-control') }} {% for error in form.username.errors %}
{{ error }}
{% endfor %}
{{ form.password.label(class='form-label') }} {{ form.password(class='form-control') }} {% for error in form.password.errors %}
{{ error }}
{{ error }}
{% endfor %}
{{ form.remember_me.label(class='form-check-label') }} {{ form.remember_me(class='form-check-input') }}
{{ form.submit(class='btn btn-primary') }}

New User? Register here

{% endblock %} {% extends 'base.html' %} {% block content %}

Register

{{ form.hidden_tag() }}
{{ form.username.label(class='form-label') }} {{ form.username(class='form-control') }} {% for error in form.username.errors %}
{{ error }}
{% endfor %}
{{ form.email.label(class='form-label') }} {{ form.email(class='form-control') }} {% for error in form.email.errors %}
{{ error }}
{% endfor %}
{{ form.password.label(class='form-label') }} {{ form.password(class='form-control') }} {% for error in form.password.errors %}
{{ error }}
{% endfor %}
{{ form.password2.label(class='form-label') }} {{ form.password2(class='form-control') }} {% for error in form.password2.errors %}
{{ error }}
{% endfor %}
{{ form.submit(class='btn btn-primary') }}

Already have an account? Sign in

{% endblock %} {% extends 'base.html' %} {% block content %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}

{{ book.title }}

by {{ book.author }}

{% if book.publication_year %}

Publication Year: {{ book.publication_year }}

{% endif %} {% if book.publisher %}

Publisher: {{ book.publisher }}

{% endif %} {% if book.isbn %}

ISBN: {{ book.isbn }}

{% endif %} {% if book.genre %}

Genre: {{ book.genre }}

{% endif %}
Rating: {% for i in range(5) %} {% if i < book.average_rating|int %} {% elif i < book.average_rating|round(0, 'ceil')|int and (book.average_rating % 1) > 0 %} {% else %} {% endif %} {% endfor %} ({{ book.average_rating|round(1) }} / 5)
{% if book.description %}

Description

{{ book.description }}

{% endif %}
{% if current_user.is_authenticated %} {% if book.user_id == current_user.id %} Edit {% endif %} {% endif %}

Reviews

{% if current_user.is_authenticated %}
{% if user_review %}Edit Your Review{% else %}Write a Review{% endif %}
{{ review_form.hidden_tag() }}
{{ review_form.rating.label(class='form-label') }} {{ review_form.rating(class='form-select') }}
{{ review_form.content.label(class='form-label') }} {{ review_form.content(class='form-control', rows=4) }}
{{ review_form.submit(class='btn btn-primary') }}
{% endif %} {% if book.reviews.all() %} {% for review in book.reviews %}
{% for i in range(5) %} {% if i < review.rating %} {% else %} {% endif %} {% endfor %}
By {{ review.author.username }} on {{ review.created_at.strftime('%Y-%m-%d') }}

{{ review.content }}

{% endfor %} {% else %}
No reviews yet. Be the first to review this book!
{% endif %}
{% endblock %} {% extends 'base.html' %} {% block content %}

Add New Book

{{ form.hidden_tag() }}
{{ form.title.label(class='form-label') }} {{ form.title(class='form-control') }} {% for error in form.title.errors %}
{{ error }}
{% endfor %}
{{ form.author.label(class='form-label') }} {{ form.author(class='form-control') }} {% for error in form.author.errors %}
{{ error }}
{% endfor %}
{{ form.isbn.label(class='form-label') }} {{ form.isbn(class='form-control') }} {% for error in form.isbn.errors %}
{{ error }}
{% endfor %}
{{ form.publisher.label(class='form-label') }} {{ form.publisher(class='form-control') }} {% for error in form.publisher.errors %}
{{ error }}
{% endfor %}
{{ form.publication_year.label(class='form-label') }} {{ form.publication_year(class='form-control') }} {% for error in form.publication_year.errors %}
{{ error }}
{% endfor %}
{{ form.genre.label(class='form-label') }} {{ form.genre(class='form-control') }} {% for error in form.genre.errors %}
{{ error }}
{% endfor %}
{{ form.cover_image.label(class='form-label') }} {{ form.cover_image(class='form-control') }} {% for error in form.cover_image.errors %}
{{ error }}
{% endfor %}
Enter a URL for the book cover image.
{{ form.description.label(class='form-label') }} {{ form.description(class='form-control', rows=5) }} {% for error in form.description.errors %}
{{ error }}
{% endfor %}
{{ form.submit(class='btn btn-primary') }} Cancel
{% endblock %} {% extends 'base.html' %} {% block content %}

Edit Book

{{ form.hidden_tag() }}
{{ form.title.label(class='form-label') }} {{ form.title(class='form-control') }} {% for error in form.title.errors %}
{{ error }}
{% endfor %}
{{ form.author.label(class='form-label') }} {{ form.author(class='form-control') }} {% for error in form.author.errors %}
{{ error }}
{% endfor %}
{{ form.isbn.label(class='form-label') }} {{ form.isbn(class='form-control') }} {% for error in form.isbn.errors %}
{{ error }}
{% endfor %}
{{ form.publisher.label(class='form-label') }} {{ form.publisher(class='form-control') }} {% for error in form.publisher.errors %}
{{ error }}
{% endfor %}
{{ form.publication_year.label(class='form-label') }} {{ form.publication_year(class='form-control') }} {% for error in form.publication_year.errors %}
{{ error }}
{% endfor %}
{{ form.genre.label(class='form-label') }} {{ form.genre(class='form-control') }} {% for error in form.genre.errors %}
{{ error }}
{% endfor %}
{{ form.cover_image.label(class='form-label') }} {{ form.cover_image(class='form-control') }} {% for error in form.cover_image.errors %}
{{ error }}
{% endfor %}
Enter a URL for the book cover image.
{{ form.description.label(class='form-label') }} {{ form.description(class='form-control', rows=5) }} {% for error in form.description.errors %}
{{ error }}
{% endfor %}
{{ form.submit(class='btn btn-primary') }} Cancel
{% endblock %} {% extends 'base.html' %} {% block content %}

Books Library

{% if current_user.is_authenticated %} Add New Book {% endif %}
{% if books.items %}
{% for book in books.items %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}
{{ book.title }}
{{ book.author }}
{% for i in range(5) %} {% if i < book.average_rating|int %} {% elif i < book.average_rating|round(0, 'ceil')|int and (book.average_rating % 1) > 0 %} {% else %} {% endif %} {% endfor %} ({{ book.average_rating|round(1) }})
{% if book.genre %}

{{ book.genre }}

{% endif %}

{{ book.description|truncate(100) }}

View Details
{% endfor %}
{% else %}
No books found. {% if current_user.is_authenticated %}Add a book{% endif %}
{% endif %} {% endblock %} {% extends 'base.html' %} {% block content %}

Search Books

{{ form.hidden_tag() }}
{{ form.query(class='form-control', placeholder='Search for books...') }}
{{ form.category(class='form-select') }}
{{ form.submit(class='btn btn-primary w-100') }}
{% if results %}

Search Results

{% for book in results %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}
{{ book.title }}
{{ book.author }}
{% for i in range(5) %} {% if i < book.average_rating|int %} {% elif i < book.average_rating|round(0, 'ceil')|int and (book.average_rating % 1) > 0 %} {% else %} {% endif %} {% endfor %}
{% if book.genre %}

{{ book.genre }}

{% endif %} View Details
{% endfor %}
{% elif request.method == 'POST' or request.args.get('query') %}
No books found matching your search criteria.
{% endif %} {% endblock %} {% extends 'base.html' %} {% block content %}

Create Reading List

{{ form.hidden_tag() }}
{{ form.name.label(class='form-label') }} {{ form.name(class='form-control') }} {% for error in form.name.errors %}
{{ error }}
{% endfor %}
{{ form.description.label(class='form-label') }} {{ form.description(class='form-control', rows=3) }} {% for error in form.description.errors %}
{{ error }}
{% endfor %}
{{ form.submit(class='btn btn-primary') }} Cancel
{% endblock %} {% extends 'base.html' %} {% block content %}

{{ reading_list.name }}

Edit List
{% if reading_list.description %}

{{ reading_list.description }}

{% endif %}

Books in this List

{% if reading_list.books.all() %}
{% for book in reading_list.books %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}
{{ book.title }}
{{ book.author }}
{% for i in range(5) %} {% if i < book.average_rating|int %} {% elif i < book.average_rating|round(0, 'ceil')|int and (book.average_rating % 1) > 0 %} {% else %} {% endif %} {% endfor %}
View
{% endfor %}
{% else %}
No books in this reading list yet. Browse books to add some!
{% endif %} {% endblock %} {% extends 'base.html' %} {% block content %}

Edit Reading List

{{ form.hidden_tag() }}
{{ form.name.label(class='form-label') }} {{ form.name(class='form-control') }} {% for error in form.name.errors %}
{{ error }}
{% endfor %}
{{ form.description.label(class='form-label') }} {{ form.description(class='form-control', rows=3) }} {% for error in form.description.errors %}
{{ error }}
{% endfor %}
{{ form.submit(class='btn btn-primary') }} Cancel
{% endblock %} {% extends 'base.html' %} {% block content %}

My Reading Lists

Create New List
{% if reading_lists %}
{% for reading_list in reading_lists %}
{{ reading_list.name }}
{% if reading_list.description %}

{{ reading_list.description|truncate(100) }}

{% endif %}

{{ reading_list.books.count() }} books

View List
{% endfor %}
{% else %}
You don't have any reading lists yet. Create one to start organizing your books!
{% endif %} {% endblock %} {% extends 'base.html' %} {% block content %}
Profile Information

Username: {{ current_user.username }}

Email: {{ current_user.email }}

Joined: {{ current_user.joined_at.strftime('%Y-%m-%d') }}

Activity Summary

{{ current_user.books.count() }}

Books Added

{{ current_user.reading_lists.count() }}

Reading Lists

{{ current_user.reviews.count() }}

Reviews Written

Recently Added Books

{% set recent_books = current_user.books.order_by(Book.created_at.desc()).limit(3).all() %} {% if recent_books %}
{% for book in recent_books %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}
{{ book.title }}

{{ book.author }}

View
{% endfor %}
{% else %}
You haven't added any books yet. Add your first book!
{% endif %}
{% endblock %} {% extends 'base.html' %} {% block content %}

About Bookshelf

Project Overview

Bookshelf is a personal library management system built with Flask. This application allows users to track their books, manage reading lists, rate and review books, and search for new titles.

Features

  • User Accounts: Register, log in, and manage your personal library
  • Book Management: Add, edit, and organize your book collection
  • Reading Lists: Create custom lists for different categories or reading goals
  • Reviews & Ratings: Rate books and write reviews
  • Search: Find books by title, author, genre, or ISBN
  • RESTful API: Access your book data programmatically

Technologies Used

  • Flask: Web framework
  • SQLAlchemy: Database ORM
  • Flask-Login: User authentication
  • Flask-WTF: Form handling
  • Flask-RESTful & Marshmallow: API development
  • Bootstrap: Frontend styling

About the Developer

Bookshelf was developed as a weekend project for Module 17 of the Comprehensive Full Stack Web Development course. The goal was to apply Flask development concepts in a practical, feature-rich application.

{% endblock %} /* Custom styles */ .jumbotron { padding: 2rem; background-color: #f7f7f9; border-radius: 0.3rem; margin-bottom: 2rem; } .book-cover { height: 200px; object-fit: cover; } .card-img-top { height: 200px; object-fit: cover; } /* Star Rating */ .star-rating { color: #ffc107; } /* Footer */ footer { margin-top: 3rem; padding: 1.5rem 0; background-color: #f8f9fa; } /* app/static/js/main.js */ // Custom JavaScript document.addEventListener('DOMContentLoaded', function() { // Initialize Bootstrap components var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) }) // Add event listeners for forms if needed });

3. Carry Out the Plan

Now let's implement our plan step by step, providing code and explanations for each part:

Step 1: Set Up Project Structure

First, let's create our project directory and set up the basic structure:

# Create project directory
$ mkdir bookshelf
$ cd bookshelf

# Create virtual environment
$ python -m venv venv
$ source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install required packages
$ pip install flask flask-sqlalchemy flask-migrate flask-login flask-wtf flask-restful flask-marshmallow marshmallow-sqlalchemy python-dotenv email-validator

Now, let's create our basic project structure:

bookshelf/
├── app/
│   ├── __init__.py
│   ├── config.py
│   ├── extensions.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── book.py
│   │   ├── reading_list.py
│   │   └── review.py
│   ├── blueprints/
│   │   ├── __init__.py
│   │   ├── auth/
│   │   │   ├── __init__.py
│   │   │   ├── routes.py
│   │   │   └── forms.py
│   │   ├── main/
│   │   │   ├── __init__.py
│   │   │   └── routes.py
│   │   ├── books/
│   │   │   ├── __init__.py
│   │   │   ├── routes.py
│   │   │   └── forms.py
│   │   ├── reading_lists/
│   │   │   ├── __init__.py
│   │   │   ├── routes.py
│   │   │   └── forms.py
│   │   └── api/
│   │       ├── __init__.py
│   │       ├── resources.py
│   │       └── schemas.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── auth/
│   │   ├── main/
│   │   ├── books/
│   │   └── reading_lists/
│   └── static/
│       ├── css/
│       └── js/
├── migrations/
├── .env
└── run.py

Now, let's implement the basic Flask application:

# app/config.py
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///bookshelf.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False


# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
login.login_message = 'Please log in to access this page.'
ma = Marshmallow()


# app/__init__.py
from flask import Flask
from app.config import Config
from app.extensions import db, migrate, login, ma

def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)

# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
ma.init_app(app)

# Register blueprints
from app.blueprints.auth import auth
from app.blueprints.main import main
from app.blueprints.books import books
from app.blueprints.reading_lists import reading_lists
from app.blueprints.api import api_bp

app.register_blueprint(auth)
app.register_blueprint(main)
app.register_blueprint(books)
app.register_blueprint(reading_lists)
app.register_blueprint(api_bp, url_prefix='/api')

return app


# app/models/__init__.py
from app.models.user import User
from app.models.book import Book
from app.models.reading_list import ReadingList, reading_list_books
from app.models.review import Review


# run.py
from app import create_app, db
from app.models import User, Book, ReadingList, Review

app = create_app()

@app.shell_context_processor
def make_shell_context():
return {
  'db': db,
  'User': User,
  'Book': Book,
  'ReadingList': ReadingList,
  'Review': Review
}

if __name__ == '__main__':
app.run(debug=True)

Step 2: Design Database Models

Now let's define our database models:

# app/models/user.py
from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app.extensions import db, login

class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(128), nullable=False)
joined_at = db.Column(db.DateTime, default=datetime.utcnow)

# Relationships
books = db.relationship('Book', backref='owner', lazy='dynamic', cascade='all, delete-orphan')
reading_lists = db.relationship('ReadingList', backref='owner', lazy='dynamic', cascade='all, delete-orphan')
reviews = db.relationship('Review', backref='author', lazy='dynamic', cascade='all, delete-orphan')

def set_password(self, password):
  self.password_hash = generate_password_hash(password)
  
def check_password(self, password):
  return check_password_hash(self.password_hash, password)

def __repr__(self):
  return f''

@login.user_loader
def load_user(id):
return User.query.get(int(id))


# app/models/book.py
from datetime import datetime
from app.extensions import db

class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False, index=True)
author = db.Column(db.String(100), nullable=False, index=True)
isbn = db.Column(db.String(20), index=True)
publisher = db.Column(db.String(100))
publication_year = db.Column(db.Integer)
description = db.Column(db.Text)
genre = db.Column(db.String(50), index=True)
cover_image = db.Column(db.String(200))
created_at = db.Column(db.DateTime, default=datetime.utcnow)

# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

# Relationships
reviews = db.relationship('Review', backref='book', lazy='dynamic', cascade='all, delete-orphan')

def __repr__(self):
  return f''

@property
def average_rating(self):
  reviews = self.reviews.all()
  if not reviews:
      return 0
  return sum(review.rating for review in reviews) / len(reviews)


# app/models/reading_list.py
from datetime import datetime
from app.extensions import db

# Association table for the many-to-many relationship
reading_list_books = db.Table('reading_list_books',
db.Column('reading_list_id', db.Integer, db.ForeignKey('reading_list.id'), primary_key=True),
db.Column('book_id', db.Integer, db.ForeignKey('book.id'), primary_key=True),
db.Column('added_at', db.DateTime, default=datetime.utcnow)
)

class ReadingList(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)

# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

# Relationships
books = db.relationship('Book', secondary=reading_list_books, lazy='dynamic',
                    backref=db.backref('reading_lists', lazy='dynamic'))

def __repr__(self):
  return f''


# app/models/review.py
from datetime import datetime
from app.extensions import db

class Review(db.Model):
id = db.Column(db.Integer, primary_key=True)
rating = db.Column(db.Integer, nullable=False)  # 1-5 stars
content = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
book_id = db.Column(db.Integer, db.ForeignKey('book.id'), nullable=False)

# Ensure a user can only review a book once
__table_args__ = (
  db.UniqueConstraint('user_id', 'book_id', name='uix_user_book_review'),
)

def __repr__(self):
  return f''

Step 3: Implement Authentication

Let's implement user authentication:

# app/blueprints/auth/__init__.py
from flask import Blueprint

auth = Blueprint('auth', __name__, url_prefix='/auth')

from app.blueprints.auth import routes


# app/blueprints/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
from app.models import User

class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=64)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
password2 = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')

def validate_username(self, username):
  user = User.query.filter_by(username=username.data).first()
  if user is not None:
      raise ValidationError('Please use a different username.')

def validate_email(self, email):
  user = User.query.filter_by(email=email.data).first()
  if user is not None:
      raise ValidationError('Please use a different email address.')

class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')


# app/blueprints/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user, login_required
from app.extensions import db
from app.blueprints.auth import auth
from app.blueprints.auth.forms import LoginForm, RegistrationForm
from app.models import User

@auth.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
  return redirect(url_for('main.index'))

form = RegistrationForm()
if form.validate_on_submit():
  user = User(username=form.username.data, email=form.email.data)
  user.set_password(form.password.data)
  db.session.add(user)
  db.session.commit()
  flash('Congratulations, you are now a registered user!', 'success')
  return redirect(url_for('auth.login'))

return render_template('auth/register.html', title='Register', form=form)

@auth.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
  return redirect(url_for('main.index'))

form = LoginForm()
if form.validate_on_submit():
  user = User.query.filter_by(username=form.username.data).first()
  if user is None or not user.check_password(form.password.data):
      flash('Invalid username or password', 'danger')
      return redirect(url_for('auth.login'))
  
  login_user(user, remember=form.remember_me.data)
  
  # Redirect to requested page or default to home page
  next_page = request.args.get('next')
  if not next_page or not next_page.startswith('/'):
      next_page = url_for('main.index')
  
  return redirect(next_page)

return render_template('auth/login.html', title='Sign In', form=form)

@auth.route('/logout')
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('main.index'))

@auth.route('/profile')
@login_required
def profile():
return render_template('auth/profile.html', title='Profile')

Step 4-6: Implement Main Features

Now let's implement the main features of our application, including book management, reading lists, and reviews:

# app/blueprints/main/__init__.py
from flask import Blueprint

main = Blueprint('main', __name__)

from app.blueprints.main import routes


# app/blueprints/main/routes.py
from flask import render_template, redirect, url_for
from app.blueprints.main import main
from app.models import Book, ReadingList, Review

@main.route('/')
def index():
recent_books = Book.query.order_by(Book.created_at.desc()).limit(5).all()
top_rated_books = Book.query.join(Review).group_by(Book.id).order_by(db.func.avg(Review.rating).desc()).limit(5).all()

return render_template('main/index.html', title='Home', 
                    recent_books=recent_books, top_rated_books=top_rated_books)

@main.route('/about')
def about():
return render_template('main/about.html', title='About')


# app/blueprints/books/__init__.py
from flask import Blueprint

books = Blueprint('books', __name__, url_prefix='/books')

from app.blueprints.books import routes


# app/blueprints/books/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, IntegerField, SelectField, SubmitField
from wtforms.validators import DataRequired, Length, Optional, NumberRange, URL

class BookForm(FlaskForm):
title = StringField('Title', validators=[DataRequired(), Length(max=200)])
author = StringField('Author', validators=[DataRequired(), Length(max=100)])
isbn = StringField('ISBN', validators=[Optional(), Length(max=20)])
publisher = StringField('Publisher', validators=[Optional(), Length(max=100)])
publication_year = IntegerField('Publication Year', validators=[Optional(), NumberRange(min=1000, max=2100)])
description = TextAreaField('Description', validators=[Optional()])
genre = StringField('Genre', validators=[Optional(), Length(max=50)])
cover_image = StringField('Cover Image URL', validators=[Optional(), URL()])
submit = SubmitField('Save Book')

class SearchForm(FlaskForm):
query = StringField('Search', validators=[DataRequired()])
category = SelectField('Category', choices=[
  ('title', 'Title'),
  ('author', 'Author'),
  ('genre', 'Genre'),
  ('isbn', 'ISBN')
])
submit = SubmitField('Search')

class ReviewForm(FlaskForm):
rating = SelectField('Rating', choices=[(1, '1 Star'), (2, '2 Stars'), (3, '3 Stars'), 
                                   (4, '4 Stars'), (5, '5 Stars')], coerce=int)
content = TextAreaField('Review', validators=[Optional(), Length(max=1000)])
submit = SubmitField('Submit Review')


# app/blueprints/books/routes.py
from flask import render_template, redirect, url_for, flash, request, abort
from flask_login import current_user, login_required
from app.extensions import db
from app.blueprints.books import books
from app.blueprints.books.forms import BookForm, SearchForm, ReviewForm
from app.models import Book, Review

@books.route('/')
def index():
page = request.args.get('page', 1, type=int)
books_list = Book.query.order_by(Book.title).paginate(page=page, per_page=12)
return render_template('books/index.html', title='All Books', books=books_list)

@books.route('/create', methods=['GET', 'POST'])
@login_required
def create():
form = BookForm()
if form.validate_on_submit():
  book = Book(
      title=form.title.data,
      author=form.author.data,
      isbn=form.isbn.data,
      publisher=form.publisher.data,
      publication_year=form.publication_year.data,
      description=form.description.data,
      genre=form.genre.data,
      cover_image=form.cover_image.data,
      user_id=current_user.id
  )
  db.session.add(book)
  db.session.commit()
  flash('Your book has been added!', 'success')
  return redirect(url_for('books.detail', id=book.id))

return render_template('books/create.html', title='Add Book', form=form)

@books.route('/')
def detail(id):
book = Book.query.get_or_404(id)
review_form = ReviewForm()
user_review = None

if current_user.is_authenticated:
  user_review = Review.query.filter_by(user_id=current_user.id, book_id=book.id).first()

return render_template('books/detail.html', title=book.title, book=book, 
                    review_form=review_form, user_review=user_review)

@books.route('//edit', methods=['GET', 'POST'])
@login_required
def edit(id):
book = Book.query.get_or_404(id)

# Check if the current user is the owner of the book
if book.user_id != current_user.id:
  abort(403)

form = BookForm()

if form.validate_on_submit():
  book.title = form.title.data
  book.author = form.author.data
  book.isbn = form.isbn.data
  book.publisher = form.publisher.data
  book.publication_year = form.publication_year.data
  book.description = form.description.data
  book.genre = form.genre.data
  book.cover_image = form.cover_image.data
  
  db.session.commit()
  flash('Your book has been updated!', 'success')
  return redirect(url_for('books.detail', id=book.id))

elif request.method == 'GET':
  form.title.data = book.title
  form.author.data = book.author
  form.isbn.data = book.isbn
  form.publisher.data = book.publisher
  form.publication_year.data = book.publication_year
  form.description.data = book.description
  form.genre.data = book.genre
  form.cover_image.data = book.cover_image

return render_template('books/edit.html', title='Edit Book', form=form, book=book)

@books.route('//delete', methods=['POST'])
@login_required
def delete(id):
book = Book.query.get_or_404(id)

# Check if the current user is the owner of the book
if book.user_id != current_user.id:
  abort(403)

db.session.delete(book)
db.session.commit()
flash('Your book has been deleted!', 'success')
return redirect(url_for('books.index'))

@books.route('/search', methods=['GET', 'POST'])
def search():
form = SearchForm()
results = []

if form.validate_on_submit() or request.args.get('query'):
  query = form.query.data if form.validate_on_submit() else request.args.get('query')
  category = form.category.data if form.validate_on_submit() else request.args.get('category', 'title')
  
  if category == 'title':
      results = Book.query.filter(Book.title.ilike(f'%{query}%')).all()
  elif category == 'author':
      results = Book.query.filter(Book.author.ilike(f'%{query}%')).all()
  elif category == 'genre':
      results = Book.query.filter(Book.genre.ilike(f'%{query}%')).all()
  elif category == 'isbn':
      results = Book.query.filter(Book.isbn.ilike(f'%{query}%')).all()

return render_template('books/search.html', title='Search Books', form=form, results=results)

@books.route('//review', methods=['POST'])
@login_required
def add_review(id):
book = Book.query.get_or_404(id)
form = ReviewForm()

if form.validate_on_submit():
  # Check if user already has a review for this book
  existing_review = Review.query.filter_by(user_id=current_user.id, book_id=book.id).first()
  
  if existing_review:
      # Update existing review
      existing_review.rating = form.rating.data
      existing_review.content = form.content.data
      flash('Your review has been updated!', 'success')
  else:
      # Create new review
      review = Review(
          rating=form.rating.data,
          content=form.content.data,
          user_id=current_user.id,
          book_id=book.id
      )
      db.session.add(review)
      flash('Your review has been added!', 'success')
  
  db.session.commit()

return redirect(url_for('books.detail', id=book.id))


# app/blueprints/reading_lists/__init__.py
from flask import Blueprint

reading_lists = Blueprint('reading_lists', __name__, url_prefix='/reading-lists')

from app.blueprints.reading_lists import routes


# app/blueprints/reading_lists/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length, Optional

class ReadingListForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=100)])
description = TextAreaField('Description', validators=[Optional(), Length(max=500)])
submit = SubmitField('Save')


# app/blueprints/reading_lists/routes.py
from flask import render_template, redirect, url_for, flash, request, abort
from flask_login import current_user, login_required
from app.extensions import db
from app.blueprints.reading_lists import reading_lists
from app.blueprints.reading_lists.forms import ReadingListForm
from app.models import ReadingList, Book

@reading_lists.route('/')
@login_required
def index():
lists = ReadingList.query.filter_by(user_id=current_user.id).all()
return render_template('reading_lists/index.html', title='My Reading Lists', reading_lists=lists)

@reading_lists.route('/create', methods=['GET', 'POST'])
@login_required
def create():
form = ReadingListForm()

if form.validate_on_submit():
  reading_list = ReadingList(
      name=form.name.data,
      description=form.description.data,
      user_id=current_user.id
  )
  db.session.add(reading_list)
  db.session.commit()
  flash('Your reading list has been created!', 'success')
  return redirect(url_for('reading_lists.detail', id=reading_list.id))

return render_template('reading_lists/create.html', title='Create Reading List', form=form)

@reading_lists.route('/')
@login_required
def detail(id):
reading_list = ReadingList.query.get_or_404(id)

# Ensure the current user is the owner of the reading list
if reading_list.user_id != current_user.id:
  abort(403)

return render_template('reading_lists/detail.html', title=reading_list.name, reading_list=reading_list)

@reading_lists.route('//edit', methods=['GET', 'POST'])
@login_required
def edit(id):
reading_list = ReadingList.query.get_or_404(id)

# Ensure the current user is the owner of the reading list
if reading_list.user_id != current_user.id:
  abort(403)

form = ReadingListForm()

if form.validate_on_submit():
  reading_list.name = form.name.data
  reading_list.description = form.description.data
  db.session.commit()
  flash('Your reading list has been updated!', 'success')
  return redirect(url_for('reading_lists.detail', id=reading_list.id))

elif request.method == 'GET':
  form.name.data = reading_list.name
  form.description.data = reading_list.description

return render_template('reading_lists/edit.html', title='Edit Reading List', form=form, reading_list=reading_list)

@reading_lists.route('//delete', methods=['POST'])
@login_required
def delete(id):
reading_list = ReadingList.query.get_or_404(id)

# Ensure the current user is the owner of the reading list
if reading_list.user_id != current_user.id:
  abort(403)

db.session.delete(reading_list)
db.session.commit()
flash('Your reading list has been deleted!', 'success')
return redirect(url_for('reading_lists.index'))

@reading_lists.route('//add/', methods=['POST'])
@login_required
def add_book(list_id, book_id):
reading_list = ReadingList.query.get_or_404(list_id)
book = Book.query.get_or_404(book_id)

# Ensure the current user is the owner of the reading list
if reading_list.user_id != current_user.id:
  abort(403)

# Check if the book is already in the reading list
if book in reading_list.books:
  flash('This book is already in the reading list.', 'info')
else:
  reading_list.books.append(book)
  db.session.commit()
  flash('Book added to the reading list!', 'success')

return redirect(url_for('reading_lists.detail', id=reading_list.id))

@reading_lists.route('//remove/', methods=['POST'])
@login_required
def remove_book(list_id, book_id):
reading_list = ReadingList.query.get_or_404(list_id)
book = Book.query.get_or_404(book_id)

# Ensure the current user is the owner of the reading list
if reading_list.user_id != current_user.id:
  abort(403)

# Check if the book is in the reading list
if book in reading_list.books:
  reading_list.books.remove(book)
  db.session.commit()
  flash('Book removed from the reading list.', 'success')
else:
  flash('This book is not in the reading list.', 'info')

return redirect(url_for('reading_lists.detail', id=reading_list.id))

Step 7: Design and Implement API

Now let's implement our RESTful API:

# app/blueprints/api/__init__.py
from flask import Blueprint

api_bp = Blueprint('api', __name__)

from app.blueprints.api import resources


# app/blueprints/api/schemas.py
from app.extensions import ma
from app.models import User, Book, ReadingList, Review
from marshmallow import fields, validates, ValidationError

class UserSchema(ma.SQLAlchemyAutoSchema):
class Meta:
  model = User
  exclude = ('password_hash',)
  load_instance = True

class ReviewSchema(ma.SQLAlchemyAutoSchema):
class Meta:
  model = Review
  include_fk = True
  load_instance = True

author = fields.Nested('UserSchema', only=('id', 'username'))

class BookSchema(ma.SQLAlchemyAutoSchema):
class Meta:
  model = Book
  include_fk = True
  load_instance = True

reviews = fields.Nested(ReviewSchema, many=True, exclude=('book',))
owner = fields.Nested('UserSchema', only=('id', 'username'))
average_rating = fields.Float()

@validates('publication_year')
def validate_year(self, value):
  if value and (value < 1000 or value > 2100):
      raise ValidationError('Publication year must be between 1000 and 2100')

class ReadingListSchema(ma.SQLAlchemyAutoSchema):
class Meta:
  model = ReadingList
  include_fk = True
  load_instance = True

books = fields.Nested(BookSchema, many=True, exclude=('reviews', 'reading_lists'))
owner = fields.Nested('UserSchema', only=('id', 'username'))

# Create schema instances
user_schema = UserSchema()
users_schema = UserSchema(many=True)
book_schema = BookSchema()
books_schema = BookSchema(many=True)
review_schema = ReviewSchema()
reviews_schema = ReviewSchema(many=True)
reading_list_schema = ReadingListSchema()
reading_lists_schema = ReadingListSchema(many=True)


# app/blueprints/api/resources.py
from flask import request, jsonify, g
from flask_restful import Api, Resource, abort
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from app.extensions import db
from app.blueprints.api import api_bp
from app.blueprints.api.schemas import (
user_schema, users_schema, book_schema, books_schema,
review_schema, reviews_schema, reading_list_schema, reading_lists_schema
)
from app.models import User, Book, ReadingList, Review
from marshmallow import ValidationError

# Set up authentication
basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth()

@basic_auth.verify_password
def verify_password(username, password):
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
  g.user = user
  return user

@token_auth.verify_token
def verify_token(token):
# In a real app, you would implement token verification here
# For simplicity, we'll use basic auth for this example
return None

# Create the API
api = Api(api_bp)

# Resource classes
class BookListAPI(Resource):
def get(self):
  # Get query parameters for filtering
  author = request.args.get('author')
  genre = request.args.get('genre')
  
  # Start with base query
  query = Book.query
  
  # Apply filters
  if author:
      query = query.filter(Book.author.ilike(f'%{author}%'))
  if genre:
      query = query.filter(Book.genre.ilike(f'%{genre}%'))
  
  # Execute query
  books = query.all()
  
  return books_schema.dump(books)

@basic_auth.login_required
def post(self):
  try:
      # Get JSON data
      json_data = request.get_json()
      
      # Ensure user_id is set to current user
      json_data['user_id'] = g.user.id
      
      # Validate and deserialize input
      book = book_schema.load(json_data)
      
      # Save the book
      db.session.add(book)
      db.session.commit()
      
      return book_schema.dump(book), 201
  
  except ValidationError as err:
      return {'errors': err.messages}, 400

class BookAPI(Resource):
def get(self, id):
  book = Book.query.get_or_404(id)
  return book_schema.dump(book)

@basic_auth.login_required
def put(self, id):
  book = Book.query.get_or_404(id)
  
  # Check if the current user is the owner
  if book.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  try:
      # Get JSON data
      json_data = request.get_json()
      
      # Validate and deserialize input
      book = book_schema.load(json_data, instance=book, partial=True)
      
      # Save the book
      db.session.commit()
      
      return book_schema.dump(book)
  
  except ValidationError as err:
      return {'errors': err.messages}, 400

@basic_auth.login_required
def delete(self, id):
  book = Book.query.get_or_404(id)
  
  # Check if the current user is the owner
  if book.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  db.session.delete(book)
  db.session.commit()
  
  return '', 204

class ReadingListListAPI(Resource):
@basic_auth.login_required
def get(self):
  reading_lists = ReadingList.query.filter_by(user_id=g.user.id).all()
  return reading_lists_schema.dump(reading_lists)

@basic_auth.login_required
def post(self):
  try:
      # Get JSON data
      json_data = request.get_json()
      
      # Ensure user_id is set to current user
      json_data['user_id'] = g.user.id
      
      # Validate and deserialize input
      reading_list = reading_list_schema.load(json_data)
      
      # Save the reading list
      db.session.add(reading_list)
      db.session.commit()
      
      return reading_list_schema.dump(reading_list), 201
  
  except ValidationError as err:
      return {'errors': err.messages}, 400

class ReadingListAPI(Resource):
@basic_auth.login_required
def get(self, id):
  reading_list = ReadingList.query.get_or_404(id)
  
  # Check if the current user is the owner
  if reading_list.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  return reading_list_schema.dump(reading_list)

@basic_auth.login_required
def put(self, id):
  reading_list = ReadingList.query.get_or_404(id)
  
  # Check if the current user is the owner
  if reading_list.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  try:
      # Get JSON data
      json_data = request.get_json()
      
      # Validate and deserialize input
      reading_list = reading_list_schema.load(json_data, instance=reading_list, partial=True)
      
      # Save the reading list
      db.session.commit()
      
      return reading_list_schema.dump(reading_list)
  
  except ValidationError as err:
      return {'errors': err.messages}, 400

@basic_auth.login_required
def delete(self, id):
  reading_list = ReadingList.query.get_or_404(id)
  
  # Check if the current user is the owner
  if reading_list.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  db.session.delete(reading_list)
  db.session.commit()
  
  return '', 204

class ReadingListBookAPI(Resource):
@basic_auth.login_required
def post(self, list_id, book_id):
  reading_list = ReadingList.query.get_or_404(list_id)
  book = Book.query.get_or_404(book_id)
  
  # Check if the current user is the owner of the reading list
  if reading_list.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  # Check if the book is already in the reading list
  if book in reading_list.books:
      return {'message': 'Book is already in this reading list'}, 400
  
  # Add book to reading list
  reading_list.books.append(book)
  db.session.commit()
  
  return {'message': 'Book added to reading list'}, 201

@basic_auth.login_required
def delete(self, list_id, book_id):
  reading_list = ReadingList.query.get_or_404(list_id)
  book = Book.query.get_or_404(book_id)
  
  # Check if the current user is the owner of the reading list
  if reading_list.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  # Check if the book is in the reading list
  if book not in reading_list.books:
      return {'message': 'Book is not in this reading list'}, 400
  
  # Remove book from reading list
  reading_list.books.remove(book)
  db.session.commit()
  
  return '', 204

class ReviewListAPI(Resource):
def get(self, book_id):
  book = Book.query.get_or_404(book_id)
  reviews = book.reviews.all()
  return reviews_schema.dump(reviews)

@basic_auth.login_required
def post(self, book_id):
  book = Book.query.get_or_404(book_id)
  
  try:
      # Get JSON data
      json_data = request.get_json()
      
      # Ensure user_id and book_id are set
      json_data['user_id'] = g.user.id
      json_data['book_id'] = book_id
      
      # Check if the user already has a review for this book
      existing_review = Review.query.filter_by(user_id=g.user.id, book_id=book_id).first()
      
      if existing_review:
          # Update existing review
          review = review_schema.load(json_data, instance=existing_review, partial=True)
          message = 'Review updated'
      else:
          # Create new review
          review = review_schema.load(json_data)
          db.session.add(review)
          message = 'Review created'
      
      db.session.commit()
      
      return {'message': message, 'review': review_schema.dump(review)}, 201
  
  except ValidationError as err:
      return {'errors': err.messages}, 400

class ReviewAPI(Resource):
def get(self, id):
  review = Review.query.get_or_404(id)
  return review_schema.dump(review)

@basic_auth.login_required
def put(self, id):
  review = Review.query.get_or_404(id)
  
  # Check if the current user is the author
  if review.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  try:
      # Get JSON data
      json_data = request.get_json()
      
      # Validate and deserialize input
      review = review_schema.load(json_data, instance=review, partial=True)
      
      # Save the review
      db.session.commit()
      
      return review_schema.dump(review)
  
  except ValidationError as err:
      return {'errors': err.messages}, 400

@basic_auth.login_required
def delete(self, id):
  review = Review.query.get_or_404(id)
  
  # Check if the current user is the author
  if review.user_id != g.user.id:
      abort(403, message="Permission denied")
  
  db.session.delete(review)
  db.session.commit()
  
  return '', 204

# Register resources
api.add_resource(BookListAPI, '/books')
api.add_resource(BookAPI, '/books/')
api.add_resource(ReadingListListAPI, '/reading-lists')
api.add_resource(ReadingListAPI, '/reading-lists/')
api.add_resource(ReadingListBookAPI, '/reading-lists//books/')
api.add_resource(ReviewListAPI, '/books//reviews')
api.add_resource(ReviewAPI, '/reviews/')

Step 8: Templates and Styling

We need to create templates for our application. For the sake of brevity, we'll only show a few key templates:







{% block title %}Bookshelf{% endblock %}






{% block styles %}{% endblock %}






{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
{% block content %}{% endblock %}

© 2025 Bookshelf - A Flask Weekend Project

{% block scripts %}{% endblock %} {% extends 'base.html' %} {% block content %}

Welcome to Bookshelf

Your personal library management system

{% if not current_user.is_authenticated %}

Join today to track your books, create reading lists, and more!

Sign Up Now {% endif %}

Recently Added Books

{% for book in recent_books %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}
{{ book.title }}
{{ book.author }}

{{ book.description|truncate(100) }}

View Details
{% endfor %}

Top Rated Books

{% for book in top_rated_books %}
{% if book.cover_image %} {{ book.title }} {% else %}
{% endif %}
{{ book.title }}
{{ book.author }}
{% for i in range(5) %} {% if i < book.average_rating|int %} {% elif i < book.average_rating|round(0, 'ceil')|int and (book.average_rating % 1) > 0 %} {% else %} {% endif %} {% endfor %} ({{ book.average_rating|round(1) }})
View Details
{% endfor %}
{% endblock %}

Step 9: Testing and Debugging

For testing our application, we can use Flask's built-in test client along with pytest. Here's a sample test for the authentication system:

# tests/test_auth.py
import pytest
from app import create_app, db
from app.models import User
from app.config import Config

class TestConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite://'  # Use in-memory database for testing

@pytest.fixture
def client():
app = create_app(TestConfig)
with app.test_client() as client:
  with app.app_context():
      db.create_all()
      yield client
      db.session.remove()
      db.drop_all()

def test_register(client):
response = client.post('/auth/register', data={
  'username': 'testuser',
  'email': 'test@example.com',
  'password': 'testpassword',
  'password2': 'testpassword'
}, follow_redirects=True)
assert response.status_code == 200
assert b'Congratulations, you are now a registered user!' in response.data

# Check if user was created in database
with client.application.app_context():
  user = User.query.filter_by(username='testuser').first()
  assert user is not None
  assert user.email == 'test@example.com'
  assert user.check_password('testpassword')

def test_login_logout(client):
# Create a user
with client.application.app_context():
  user = User(username='testuser', email='test@example.com')
  user.set_password('testpassword')
  db.session.add(user)
  db.session.commit()

# Test login
response = client.post('/auth/login', data={
  'username': 'testuser',
  'password': 'testpassword',
  'remember_me': False
}, follow_redirects=True)
assert response.status_code == 200

# Test protected page access
response = client.get('/reading-lists/')
assert response.status_code == 200

# Test logout
response = client.get('/auth/logout', follow_redirects=True)
assert response.status_code == 200
assert b'You have been logged out.' in response.data

# Test protected page access after logout
response = client.get('/reading-lists/')
assert response.status_code == 302  # Redirect to login

To run the tests:

$ python -m pytest -v

4. Look Back

Now that we've implemented our application, let's evaluate our solution and think about improvements and lessons learned.

What We've Accomplished

  • Created a complete Flask web application with a modular structure using blueprints
  • Implemented a database model with relationships between users, books, reading lists, and reviews
  • Added user authentication with registration, login, and profile management
  • Built CRUD functionality for books and reading lists
  • Created a review and rating system
  • Implemented search functionality
  • Developed a RESTful API with proper authentication and validation
  • Created responsive UI templates with Bootstrap
  • Added tests for key functionality

Areas for Improvement

  • Security Enhancements: Implement CSRF protection for all forms, add rate limiting to the API, use HTTPS in production
  • User Experience: Add pagination for large collections, improve mobile responsiveness, add client-side validation, implement AJAX for a smoother experience
  • Features: Add book recommendations, social features (sharing, following users), import/export functionality, book categories and tags
  • Architecture: Improve error handling, add logging, enhance API authentication with tokens, optimize database queries
  • Testing: Increase test coverage, add integration and UI tests, implement CI/CD pipeline

Lessons Learned

  • Flask's Flexibility: Flask's modular design allows for structuring applications in a way that makes sense for your specific project
  • SQLAlchemy Power: The ORM makes it easy to define complex relationships between models and work with the database
  • Blueprint Organization: Using blueprints helps keep the code organized and maintainable as the application grows
  • REST API Design: Well-structured APIs with proper validation and error handling improve the developer experience
  • Authentication Complexity: User authentication requires careful implementation to ensure security and a good user experience

Final Thoughts

This project showcases how Flask can be used to create a feature-rich web application with both a user-friendly web interface and a robust API. By following George Polya's problem-solving approach, we were able to break down a complex task into manageable steps and systematically implement a solution.

The project incorporates many of the Flask concepts covered in Module 17, including routing, templates, database operations, forms, blueprints, and APIs. The modular structure makes it easy to add new features or make improvements in the future.

As a weekend project, this implementation focuses on core functionality while leaving room for enhancements and optimizations. In a real-world scenario, you would likely spend more time on testing, security, performance, and user experience before deploying to production.

Conclusion

Congratulations on completing the Flask Weekend Project! You've built a complete web application that demonstrates your understanding of Flask and related technologies. This project has given you hands-on experience with:

  • Structuring a Flask application with blueprints
  • Working with databases using SQLAlchemy
  • Implementing user authentication
  • Creating and validating forms
  • Building RESTful APIs
  • Templating with Jinja2
  • Testing Flask applications

As you continue your journey in web development, remember that this project can serve as a foundation for more complex applications. The skills you've developed here are directly applicable to real-world web projects.

Happy coding!