Full-Stack Web Application
Learning Objectives
- By completing this project, you will:
- - Design and implement a full-stack web application
- - Create RESTful APIs
- - Implement user authentication and authorization
- - Design and manage databases
- - Build responsive user interfaces
- - Handle file uploads and media
- - Implement search and filtering
- - Deploy application to production
- - Write comprehensive tests
- - Document your code
Capstone Project 1: Full-Stack Web Application
Project Overview
Build a complete full-stack web application using Python, demonstrating mastery of web development, database management, API design, authentication, and deployment.
Project Duration: 2-3 weeks
Difficulty: Advanced
Technologies: Flask/Django, SQLAlchemy, HTML/CSS/JavaScript, Database (SQLite/PostgreSQL)
Learning Objectives
By completing this project, you will:
- Design and implement a full-stack web application
- Create RESTful APIs
- Implement user authentication and authorization
- Design and manage databases
- Build responsive user interfaces
- Handle file uploads and media
- Implement search and filtering
- Deploy application to production
- Write comprehensive tests
- Document your code
Project Requirements
Core Requirements
- Backend Framework: Use Flask or Django
- Database: SQLite (development) or PostgreSQL (production)
- Authentication: User registration, login, logout
- CRUD Operations: Create, Read, Update, Delete functionality
- API Endpoints: RESTful API design
- Frontend: Responsive web interface
- Testing: Unit tests and integration tests
- Documentation: README, API documentation, code comments
Functional Requirements
-
User Management
- User registration with validation
- User login/logout
- Password hashing
- User profiles
- Role-based access control (optional)
-
Core Features (Choose one domain):
- Blog Platform: Posts, comments, categories, tags
- Task Manager: Tasks, projects, teams, deadlines
- E-commerce: Products, cart, orders, payments (simulated)
- Social Network: Posts, followers, likes, comments
- Learning Platform: Courses, lessons, progress tracking
-
Additional Features
- Search functionality
- Pagination
- File uploads (images, documents)
- Email notifications (optional)
- API documentation
Technical Requirements
-
Code Quality
- Follow PEP 8 style guide
- Type hints where appropriate
- Docstrings for all functions/classes
- Error handling
- Input validation
-
Security
- Password hashing (bcrypt)
- CSRF protection
- SQL injection prevention
- XSS protection
- Secure session management
-
Testing
- Minimum 80% code coverage
- Unit tests
- Integration tests
- API endpoint tests
-
Deployment
- Environment configuration
- Production-ready settings
- Database migrations
- Deployment documentation
Suggested Project: Task Management Platform
Features
-
User Features
- Register/Login/Logout
- User profile management
- Dashboard
-
Task Management
- Create, read, update, delete tasks
- Task categories/tags
- Task priorities
- Due dates
- Task status (pending, in progress, completed)
-
Project Management
- Create projects
- Assign tasks to projects
- Project members (optional)
- Project progress tracking
-
Additional Features
- Search tasks
- Filter by status, priority, category
- Task statistics
- Export tasks (CSV/JSON)
Technology Stack
Backend
- Framework: Flask or Django
- ORM: SQLAlchemy (Flask) or Django ORM
- Authentication: Flask-Login or Django Auth
- API: Flask-RESTful or Django REST Framework
- Validation: WTForms or Django Forms
Database
- Development: SQLite
- Production: PostgreSQL (optional)
Frontend
- Templates: Jinja2 (Flask) or Django Templates
- Styling: Bootstrap or Tailwind CSS
- JavaScript: Vanilla JS or jQuery
- AJAX: For API interactions
Testing
- Framework: pytest or unittest
- Coverage: pytest-cov
- API Testing: requests library
Deployment
- Platform: Heroku, Railway, or DigitalOcean
- Database: PostgreSQL (production)
- Environment: Environment variables
Project Structure
Flask Structure
taskmanager/
├── README.md
├── requirements.txt
├── .env.example
├── .gitignore
├── app/
│ ├── __init__.py
│ ├── config.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ ├── task.py
│ │ └── project.py
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── tasks.py
│ │ └── projects.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── tasks.py
│ │ └── projects.py
│ ├── forms/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ └── task.py
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── helpers.py
│ │ └── validators.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── auth/
│ │ │ ├── login.html
│ │ │ └── register.html
│ │ ├── tasks/
│ │ │ ├── list.html
│ │ │ ├── create.html
│ │ │ └── detail.html
│ │ └── dashboard.html
│ ├── static/
│ │ ├── css/
│ │ ├── js/
│ │ └── images/
│ └── migrations/
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_models.py
│ ├── test_routes.py
│ └── test_api.py
└── run.py
Django Structure
taskmanager/
├── manage.py
├── requirements.txt
├── .env.example
├── taskmanager/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── accounts/
│ ├── models.py
│ ├── views.py
│ ├── urls.py
│ └── templates/
├── tasks/
│ ├── models.py
│ ├── views.py
│ ├── urls.py
│ ├── serializers.py
│ └── templates/
├── projects/
│ ├── models.py
│ ├── views.py
│ ├── urls.py
│ └── templates/
├── static/
├── media/
└── tests/
Implementation Guide
Phase 1: Setup and Configuration (Days 1-2)
Step 1: Project Setup
# Create project directory
mkdir taskmanager
cd taskmanager
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Install dependencies
pip install flask flask-sqlalchemy flask-login flask-wtf flask-migrate
pip install bcrypt python-dotenv
pip install pytest pytest-cov
# Create requirements.txt
pip freeze > requirements.txt
Step 2: Project Structure
# Create directory structure
mkdir -p app/{models,routes,api,forms,utils,templates,static/{css,js,images}}
mkdir -p tests
touch app/__init__.py app/config.py
touch app/models/__init__.py
touch app/routes/__init__.py
touch app/api/__init__.py
touch tests/__init__.py
Step 3: Configuration
# app/config.py
import os
from pathlib import Path
class Config:
"""Base configuration."""
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
SQLALCHEMY_DATABASE_URI = os.getenv(
'DATABASE_URL',
f'sqlite:///{Path(__file__).parent.parent / "taskmanager.db"}'
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Flask-Login
REMEMBER_COOKIE_DURATION = 86400 # 1 day
# File uploads
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
UPLOAD_FOLDER = Path(__file__).parent.parent / 'uploads'
# Pagination
ITEMS_PER_PAGE = 10
class DevelopmentConfig(Config):
DEBUG = True
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL')
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from app.config import config
import os
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
def create_app(config_name=None):
"""Application factory."""
app = Flask(__name__)
# Load configuration
config_name = config_name or os.getenv('FLASK_ENV', 'default')
app.config.from_object(config[config_name])
# Initialize extensions
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
migrate.init_app(app, db)
# Register blueprints
from app.routes.auth import auth_bp
from app.routes.tasks import tasks_bp
from app.routes.projects import projects_bp
from app.api import api_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(tasks_bp, url_prefix='/tasks')
app.register_blueprint(projects_bp, url_prefix='/projects')
app.register_blueprint(api_bp, url_prefix='/api')
# Create database tables
with app.app_context():
db.create_all()
return app
Phase 2: Database Models (Days 3-4)
User Model
# app/models/user.py
from app import db
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
class User(UserMixin, db.Model):
"""User model."""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
first_name = db.Column(db.String(50))
last_name = db.Column(db.String(50))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
# Relationships
tasks = db.relationship('Task', backref='user', lazy='dynamic', cascade='all, delete-orphan')
projects = db.relationship('Project', backref='user', lazy='dynamic', cascade='all, delete-orphan')
def set_password(self, password):
"""Set password hash."""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Check password."""
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
Task Model
# app/models/task.py
from app import db
from datetime import datetime
from enum import Enum
class TaskStatus(Enum):
PENDING = 'pending'
IN_PROGRESS = 'in_progress'
COMPLETED = 'completed'
CANCELLED = 'cancelled'
class TaskPriority(Enum):
LOW = 'low'
MEDIUM = 'medium'
HIGH = 'high'
URGENT = 'urgent'
class Task(db.Model):
"""Task model."""
__tablename__ = 'tasks'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
status = db.Column(db.String(20), default=TaskStatus.PENDING.value)
priority = db.Column(db.String(20), default=TaskPriority.MEDIUM.value)
due_date = db.Column(db.DateTime)
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('users.id'), nullable=False)
project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=True)
# Relationships
project = db.relationship('Project', backref='tasks')
def __repr__(self):
return f'<Task {self.title}>'
def to_dict(self):
"""Convert to dictionary."""
return {
'id': self.id,
'title': self.title,
'description': self.description,
'status': self.status,
'priority': self.priority,
'due_date': self.due_date.isoformat() if self.due_date else None,
'created_at': self.created_at.isoformat(),
'user_id': self.user_id,
'project_id': self.project_id
}
Project Model
# app/models/project.py
from app import db
from datetime import datetime
class Project(db.Model):
"""Project model."""
__tablename__ = 'projects'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
description = 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('users.id'), nullable=False)
def __repr__(self):
return f'<Project {self.name}>'
def to_dict(self):
"""Convert to dictionary."""
return {
'id': self.id,
'name': self.name,
'description': self.description,
'created_at': self.created_at.isoformat(),
'user_id': self.user_id,
'task_count': self.tasks.count()
}
# app/models/__init__.py
from app.models.user import User
from app.models.task import Task, TaskStatus, TaskPriority
from app.models.project import Project
__all__ = ['User', 'Task', 'TaskStatus', 'TaskPriority', 'Project']
Phase 3: Authentication (Days 5-6)
Login Manager
# app/__init__.py (add to existing)
from app.models import User
@login_manager.user_loader
def load_user(user_id):
"""Load user for Flask-Login."""
return User.query.get(int(user_id))
Authentication Forms
# app/forms/auth.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from app.models import User
class RegistrationForm(FlaskForm):
"""User registration form."""
username = StringField('Username', validators=[
DataRequired(),
Length(min=4, max=20, message='Username must be between 4 and 20 characters')
])
email = StringField('Email', validators=[
DataRequired(),
Email(message='Invalid email address')
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8, message='Password must be at least 8 characters')
])
password2 = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password', message='Passwords must match')
])
submit = SubmitField('Register')
def validate_username(self, username):
"""Validate username uniqueness."""
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('Username already exists. Please choose a different one.')
def validate_email(self, email):
"""Validate email uniqueness."""
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('Email already registered. Please use a different email.')
class LoginForm(FlaskForm):
"""User login form."""
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Login')
Authentication Routes
# app/routes/auth.py
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from app import db
from app.models import User
from app.forms.auth import RegistrationForm, LoginForm
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
"""User registration."""
if current_user.is_authenticated:
return redirect(url_for('tasks.list'))
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('Registration successful! Please log in.', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""User login."""
if current_user.is_authenticated:
return redirect(url_for('tasks.list'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('tasks.list'))
flash('Invalid username or password.', 'error')
return render_template('auth/login.html', form=form)
@auth_bp.route('/logout')
@login_required
def logout():
"""User logout."""
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('auth.login'))
Phase 4: Task Management (Days 7-10)
Task Forms
# app/forms/task.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, DateTimeField, SubmitField
from wtforms.validators import DataRequired, Optional
from app.models import TaskStatus, TaskPriority
class TaskForm(FlaskForm):
"""Task creation/editing form."""
title = StringField('Title', validators=[DataRequired()])
description = TextAreaField('Description')
status = SelectField('Status', choices=[
(status.value, status.value.replace('_', ' ').title())
for status in TaskStatus
])
priority = SelectField('Priority', choices=[
(priority.value, priority.value.title())
for priority in TaskPriority
])
due_date = DateTimeField('Due Date', format='%Y-%m-%d %H:%M', validators=[Optional()])
submit = SubmitField('Save Task')
Task Routes
# app/routes/tasks.py
from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify
from flask_login import login_required, current_user
from app import db
from app.models import Task, Project
from app.forms.task import TaskForm
from datetime import datetime
tasks_bp = Blueprint('tasks', __name__)
@tasks_bp.route('/')
@login_required
def list():
"""List all tasks for current user."""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', 'all')
priority = request.args.get('priority', 'all')
search = request.args.get('search', '')
query = Task.query.filter_by(user_id=current_user.id)
# Filters
if status != 'all':
query = query.filter_by(status=status)
if priority != 'all':
query = query.filter_by(priority=priority)
if search:
query = query.filter(
Task.title.contains(search) | Task.description.contains(search)
)
tasks = query.order_by(Task.created_at.desc()).paginate(
page=page, per_page=10, error_out=False
)
return render_template('tasks/list.html', tasks=tasks,
status=status, priority=priority, search=search)
@tasks_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create():
"""Create a new task."""
form = TaskForm()
form.project_id.choices = [(0, 'None')] + [
(p.id, p.name) for p in Project.query.filter_by(user_id=current_user.id).all()
]
if form.validate_on_submit():
task = Task(
title=form.title.data,
description=form.description.data,
status=form.status.data,
priority=form.priority.data,
due_date=form.due_date.data,
user_id=current_user.id,
project_id=form.project_id.data if form.project_id.data else None
)
db.session.add(task)
db.session.commit()
flash('Task created successfully!', 'success')
return redirect(url_for('tasks.detail', id=task.id))
return render_template('tasks/create.html', form=form)
@tasks_bp.route('/<int:id>')
@login_required
def detail(id):
"""View task details."""
task = Task.query.get_or_404(id)
if task.user_id != current_user.id:
flash('You do not have permission to view this task.', 'error')
return redirect(url_for('tasks.list'))
return render_template('tasks/detail.html', task=task)
@tasks_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
"""Edit a task."""
task = Task.query.get_or_404(id)
if task.user_id != current_user.id:
flash('You do not have permission to edit this task.', 'error')
return redirect(url_for('tasks.list'))
form = TaskForm(obj=task)
form.project_id.choices = [(0, 'None')] + [
(p.id, p.name) for p in Project.query.filter_by(user_id=current_user.id).all()
]
if form.validate_on_submit():
task.title = form.title.data
task.description = form.description.data
task.status = form.status.data
task.priority = form.priority.data
task.due_date = form.due_date.data
task.updated_at = datetime.utcnow()
db.session.commit()
flash('Task updated successfully!', 'success')
return redirect(url_for('tasks.detail', id=task.id))
return render_template('tasks/edit.html', form=form, task=task)
@tasks_bp.route('/<int:id>/delete', methods=['POST'])
@login_required
def delete(id):
"""Delete a task."""
task = Task.query.get_or_404(id)
if task.user_id != current_user.id:
flash('You do not have permission to delete this task.', 'error')
return redirect(url_for('tasks.list'))
db.session.delete(task)
db.session.commit()
flash('Task deleted successfully!', 'success')
return redirect(url_for('tasks.list'))
Phase 5: API Development (Days 11-12)
API Routes
# app/api/__init__.py
from flask import Blueprint
from flask_restful import Api
api_bp = Blueprint('api', __name__)
api = Api(api_bp)
from app.api import tasks, auth
# Register resources
api.add_resource(tasks.TaskListResource, '/tasks')
api.add_resource(tasks.TaskResource, '/tasks/<int:id>')
api.add_resource(auth.LoginResource, '/auth/login')
# app/api/tasks.py
from flask_restful import Resource, reqparse
from flask_login import login_required, current_user
from app import db
from app.models import Task
parser = reqparse.RequestParser()
parser.add_argument('title', type=str, required=True, help='Title is required')
parser.add_argument('description', type=str)
parser.add_argument('status', type=str)
parser.add_argument('priority', type=str)
parser.add_argument('due_date', type=str)
class TaskListResource(Resource):
"""Task list API endpoint."""
@login_required
def get(self):
"""Get all tasks for current user."""
tasks = Task.query.filter_by(user_id=current_user.id).all()
return {'tasks': [task.to_dict() for task in tasks]}, 200
@login_required
def post(self):
"""Create a new task."""
args = parser.parse_args()
task = Task(
title=args['title'],
description=args.get('description'),
status=args.get('status', 'pending'),
priority=args.get('priority', 'medium'),
user_id=current_user.id
)
db.session.add(task)
db.session.commit()
return task.to_dict(), 201
class TaskResource(Resource):
"""Single task API endpoint."""
@login_required
def get(self, id):
"""Get task by ID."""
task = Task.query.get_or_404(id)
if task.user_id != current_user.id:
return {'error': 'Unauthorized'}, 403
return task.to_dict(), 200
@login_required
def put(self, id):
"""Update task."""
task = Task.query.get_or_404(id)
if task.user_id != current_user.id:
return {'error': 'Unauthorized'}, 403
args = parser.parse_args()
task.title = args['title']
task.description = args.get('description')
task.status = args.get('status', task.status)
task.priority = args.get('priority', task.priority)
db.session.commit()
return task.to_dict(), 200
@login_required
def delete(self, id):
"""Delete task."""
task = Task.query.get_or_404(id)
if task.user_id != current_user.id:
return {'error': 'Unauthorized'}, 403
db.session.delete(task)
db.session.commit()
return {'message': 'Task deleted'}, 200
Phase 6: Frontend Templates (Days 13-14)
Base Template
<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Task Manager{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('tasks.list') }}">Task Manager</a>
<div class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<a class="nav-link" href="{{ url_for('tasks.list') }}">Tasks</a>
<a class="nav-link" href="{{ url_for('projects.list') }}">Projects</a>
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
{% else %}
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
<a class="nav-link" href="{{ url_for('auth.register') }}">Register</a>
{% endif %}
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
Task List Template
<!-- app/templates/tasks/list.html -->
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-12">
<h1>My Tasks</h1>
<a href="{{ url_for('tasks.create') }}" class="btn btn-primary mb-3">Create New Task</a>
<!-- Filters -->
<form method="GET" class="mb-3">
<div class="row">
<div class="col-md-3">
<input type="text" name="search" class="form-control" placeholder="Search..." value="{{ search }}">
</div>
<div class="col-md-2">
<select name="status" class="form-select">
<option value="all" {% if status == 'all' %}selected{% endif %}>All Status</option>
<option value="pending" {% if status == 'pending' %}selected{% endif %}>Pending</option>
<option value="in_progress" {% if status == 'in_progress' %}selected{% endif %}>In Progress</option>
<option value="completed" {% if status == 'completed' %}selected{% endif %}>Completed</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-secondary">Filter</button>
</div>
</div>
</form>
<!-- Task List -->
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Title</th>
<th>Status</th>
<th>Priority</th>
<th>Due Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for task in tasks.items %}
<tr>
<td><a href="{{ url_for('tasks.detail', id=task.id) }}">{{ task.title }}</a></td>
<td><span class="badge bg-{{ 'success' if task.status == 'completed' else 'warning' }}">{{ task.status }}</span></td>
<td><span class="badge bg-{{ 'danger' if task.priority == 'urgent' else 'info' }}">{{ task.priority }}</span></td>
<td>{{ task.due_date.strftime('%Y-%m-%d') if task.due_date else '-' }}</td>
<td>
<a href="{{ url_for('tasks.edit', id=task.id) }}" class="btn btn-sm btn-outline-primary">Edit</a>
<form method="POST" action="{{ url_for('tasks.delete', id=task.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if tasks.pages > 1 %}
<nav>
<ul class="pagination">
{% if tasks.has_prev %}
<li class="page-item"><a class="page-link" href="{{ url_for('tasks.list', page=tasks.prev_num) }}">Previous</a></li>
{% endif %}
{% for page_num in tasks.iter_pages() %}
{% if page_num %}
<li class="page-item {% if page_num == tasks.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('tasks.list', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">…</span></li>
{% endif %}
{% endfor %}
{% if tasks.has_next %}
<li class="page-item"><a class="page-link" href="{{ url_for('tasks.list', page=tasks.next_num) }}">Next</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% endblock %}
Phase 7: Testing (Days 15-16)
Test Configuration
# tests/conftest.py
import pytest
from app import create_app, db
from app.models import User, Task
@pytest.fixture
def app():
"""Create test application."""
app = create_app('testing')
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
@pytest.fixture
def user(app):
"""Create test user."""
user = User(username='testuser', email='test@example.com')
user.set_password('password123')
with app.app_context():
db.session.add(user)
db.session.commit()
return user
Test Examples
# tests/test_models.py
import pytest
from app import db
from app.models import User, Task
def test_user_creation(app):
"""Test user creation."""
with app.app_context():
user = User(username='testuser', email='test@example.com')
user.set_password('password123')
db.session.add(user)
db.session.commit()
assert user.id is not None
assert user.check_password('password123')
assert not user.check_password('wrongpassword')
def test_task_creation(app, user):
"""Test task creation."""
with app.app_context():
task = Task(
title='Test Task',
description='Test Description',
user_id=user.id
)
db.session.add(task)
db.session.commit()
assert task.id is not None
assert task.user_id == user.id
# tests/test_routes.py
import pytest
from app.models import User, Task
def test_login(client, user):
"""Test user login."""
response = client.post('/auth/login', data={
'username': 'testuser',
'password': 'password123'
}, follow_redirects=True)
assert response.status_code == 200
assert b'Task Manager' in response.data
def test_create_task(client, user):
"""Test task creation."""
# Login first
client.post('/auth/login', data={
'username': 'testuser',
'password': 'password123'
})
# Create task
response = client.post('/tasks/create', data={
'title': 'New Task',
'description': 'Task Description',
'status': 'pending',
'priority': 'medium'
}, follow_redirects=True)
assert response.status_code == 200
assert b'New Task' in response.data
Phase 8: Deployment (Days 17-18)
Environment Configuration
# .env.example
FLASK_ENV=production
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost/taskmanager
DEBUG=False
Deployment Checklist
- [ ] Set environment variables
- [ ] Configure production database
- [ ] Set up static file serving
- [ ] Configure error logging
- [ ] Set up SSL/HTTPS
- [ ] Configure domain
- [ ] Set up backups
- [ ] Monitor application
Evaluation Criteria
Functionality (40%)
- All core features implemented
- User authentication works
- CRUD operations functional
- API endpoints working
- Error handling
Code Quality (25%)
- Follows PEP 8
- Proper code organization
- Type hints and docstrings
- No code duplication
- Clean architecture
Testing (15%)
- Unit tests written
- Integration tests
- Test coverage > 80%
- Tests pass
Documentation (10%)
- README with setup instructions
- API documentation
- Code comments
- Deployment guide
Security (10%)
- Password hashing
- CSRF protection
- Input validation
- SQL injection prevention
Submission Requirements
-
GitHub Repository
- Complete source code
- README.md
- requirements.txt
- .gitignore
-
Documentation
- Setup instructions
- API documentation
- Deployment guide
-
Testing
- Test files
- Coverage report
-
Demo
- Working application
- Screenshots/video
- Live deployment (optional)
Additional Resources
- Flask Documentation: flask.palletsprojects.com
- SQLAlchemy Documentation: docs.sqlalchemy.org
- Bootstrap Documentation: getbootstrap.com
- REST API Design: restfulapi.net
- Deployment Guides: Heroku, Railway, DigitalOcean
Next Steps
After completing this project:
- Add more features (notifications, file uploads)
- Implement real-time updates (WebSockets)
- Add mobile app (React Native)
- Implement advanced search
- Add analytics and reporting
Good luck with your capstone project!
Course Navigation
Command-Line Tools
Web Scraping
Database Applications
Automation Scripts
API Development
Full-Stack Web Application
- Full-Stack Web Application
Data Analysis Project
Data Analysis Project