Migrate from Quart to Flask and add MariaDB dynamic database
Major Changes: - Migrated web framework from Quart (async) to Flask (sync) - Added MariaDB database integration with SQLAlchemy ORM - Implemented dynamic content management for portfolio New Features: - Database models for Profile, Skills, Projects, ProjectTags, and SocialLinks - RESTful API endpoints for CRUD operations on all entities - Database initialization script (init_db.py) with sample data - Docker Compose configuration with MariaDB service Modified Files: - app.py: Replaced Quart with Flask, added database initialization - config.py: Added database configuration with environment variables - routes/home.py: Converted async to sync, added database queries - requirements.txt: Replaced Quart/Hypercorn with Flask/Gunicorn, added Flask-SQLAlchemy and PyMySQL - docker-compose.yml: Added MariaDB service with health checks - templates/: Updated all templates to use dynamic data from database with Jinja2 - .env.example: Added database configuration variables - README.md: Complete rewrite with new setup instructions and API documentation New Files: - models.py: SQLAlchemy models for all database entities - init_db.py: Database initialization script - routes/api.py: REST API endpoints for content management Benefits: - Simplified architecture (sync vs async) - Better ecosystem compatibility - Dynamic content management via database - Easy content updates through REST API - Improved deployment with standard WSGI server (Gunicorn)
This commit is contained in:
239
routes/api.py
Normal file
239
routes/api.py
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright Hersel Giannella
|
||||
|
||||
"""
|
||||
API Routes for managing portfolio data dynamically
|
||||
Provides REST endpoints for CRUD operations on Profile, Skills, Projects, and Social Links
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from models import db, Profile, Skill, Project, ProjectTag, SocialLink
|
||||
|
||||
route_api = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PROFILE ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@route_api.route('/profile', methods=['GET'])
|
||||
def get_profile():
|
||||
"""Get profile information"""
|
||||
profile = Profile.query.first()
|
||||
if profile:
|
||||
return jsonify(profile.to_dict())
|
||||
return jsonify({'message': 'Profile not found'}), 404
|
||||
|
||||
|
||||
@route_api.route('/profile', methods=['PUT'])
|
||||
def update_profile():
|
||||
"""Update profile information"""
|
||||
profile = Profile.query.first()
|
||||
if not profile:
|
||||
return jsonify({'message': 'Profile not found'}), 404
|
||||
|
||||
data = request.json
|
||||
profile.title = data.get('title', profile.title)
|
||||
profile.lead_text = data.get('lead_text', profile.lead_text)
|
||||
profile.description_1 = data.get('description_1', profile.description_1)
|
||||
profile.description_2 = data.get('description_2', profile.description_2)
|
||||
profile.years_experience = data.get('years_experience', profile.years_experience)
|
||||
profile.cv_url = data.get('cv_url', profile.cv_url)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(profile.to_dict())
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SKILLS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@route_api.route('/skills', methods=['GET'])
|
||||
def get_skills():
|
||||
"""Get all skills"""
|
||||
skills = Skill.query.order_by(Skill.display_order).all()
|
||||
return jsonify([skill.to_dict() for skill in skills])
|
||||
|
||||
|
||||
@route_api.route('/skills', methods=['POST'])
|
||||
def create_skill():
|
||||
"""Create a new skill"""
|
||||
data = request.json
|
||||
skill = Skill(
|
||||
name=data['name'],
|
||||
icon_class=data['icon_class'],
|
||||
category=data.get('category'),
|
||||
proficiency_level=data.get('proficiency_level'),
|
||||
display_order=data.get('display_order', 0),
|
||||
is_active=data.get('is_active', True)
|
||||
)
|
||||
db.session.add(skill)
|
||||
db.session.commit()
|
||||
return jsonify(skill.to_dict()), 201
|
||||
|
||||
|
||||
@route_api.route('/skills/<int:skill_id>', methods=['PUT'])
|
||||
def update_skill(skill_id):
|
||||
"""Update a skill"""
|
||||
skill = Skill.query.get_or_404(skill_id)
|
||||
data = request.json
|
||||
|
||||
skill.name = data.get('name', skill.name)
|
||||
skill.icon_class = data.get('icon_class', skill.icon_class)
|
||||
skill.category = data.get('category', skill.category)
|
||||
skill.proficiency_level = data.get('proficiency_level', skill.proficiency_level)
|
||||
skill.display_order = data.get('display_order', skill.display_order)
|
||||
skill.is_active = data.get('is_active', skill.is_active)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(skill.to_dict())
|
||||
|
||||
|
||||
@route_api.route('/skills/<int:skill_id>', methods=['DELETE'])
|
||||
def delete_skill(skill_id):
|
||||
"""Delete a skill"""
|
||||
skill = Skill.query.get_or_404(skill_id)
|
||||
db.session.delete(skill)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Skill deleted successfully'})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PROJECTS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@route_api.route('/projects', methods=['GET'])
|
||||
def get_projects():
|
||||
"""Get all projects"""
|
||||
projects = Project.query.order_by(Project.display_order).all()
|
||||
return jsonify([project.to_dict() for project in projects])
|
||||
|
||||
|
||||
@route_api.route('/projects', methods=['POST'])
|
||||
def create_project():
|
||||
"""Create a new project"""
|
||||
data = request.json
|
||||
project = Project(
|
||||
title=data['title'],
|
||||
description=data['description'],
|
||||
image_url=data.get('image_url'),
|
||||
demo_url=data.get('demo_url'),
|
||||
github_url=data.get('github_url'),
|
||||
display_order=data.get('display_order', 0),
|
||||
animation_delay=data.get('animation_delay', '0s'),
|
||||
is_published=data.get('is_published', True)
|
||||
)
|
||||
db.session.add(project)
|
||||
db.session.flush()
|
||||
|
||||
# Add tags
|
||||
tags_data = data.get('tags', [])
|
||||
for tag_data in tags_data:
|
||||
tag = ProjectTag(
|
||||
project_id=project.id,
|
||||
name=tag_data['name'],
|
||||
color_class=tag_data.get('color_class', 'bg-primary'),
|
||||
display_order=tag_data.get('display_order', 0)
|
||||
)
|
||||
db.session.add(tag)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(project.to_dict()), 201
|
||||
|
||||
|
||||
@route_api.route('/projects/<int:project_id>', methods=['PUT'])
|
||||
def update_project(project_id):
|
||||
"""Update a project"""
|
||||
project = Project.query.get_or_404(project_id)
|
||||
data = request.json
|
||||
|
||||
project.title = data.get('title', project.title)
|
||||
project.description = data.get('description', project.description)
|
||||
project.image_url = data.get('image_url', project.image_url)
|
||||
project.demo_url = data.get('demo_url', project.demo_url)
|
||||
project.github_url = data.get('github_url', project.github_url)
|
||||
project.display_order = data.get('display_order', project.display_order)
|
||||
project.animation_delay = data.get('animation_delay', project.animation_delay)
|
||||
project.is_published = data.get('is_published', project.is_published)
|
||||
|
||||
# Update tags if provided
|
||||
if 'tags' in data:
|
||||
# Remove old tags
|
||||
ProjectTag.query.filter_by(project_id=project.id).delete()
|
||||
# Add new tags
|
||||
for tag_data in data['tags']:
|
||||
tag = ProjectTag(
|
||||
project_id=project.id,
|
||||
name=tag_data['name'],
|
||||
color_class=tag_data.get('color_class', 'bg-primary'),
|
||||
display_order=tag_data.get('display_order', 0)
|
||||
)
|
||||
db.session.add(tag)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(project.to_dict())
|
||||
|
||||
|
||||
@route_api.route('/projects/<int:project_id>', methods=['DELETE'])
|
||||
def delete_project(project_id):
|
||||
"""Delete a project"""
|
||||
project = Project.query.get_or_404(project_id)
|
||||
db.session.delete(project)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Project deleted successfully'})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SOCIAL LINKS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@route_api.route('/social-links', methods=['GET'])
|
||||
def get_social_links():
|
||||
"""Get all social links"""
|
||||
links = SocialLink.query.order_by(SocialLink.display_order).all()
|
||||
return jsonify([link.to_dict() for link in links])
|
||||
|
||||
|
||||
@route_api.route('/social-links', methods=['POST'])
|
||||
def create_social_link():
|
||||
"""Create a new social link"""
|
||||
data = request.json
|
||||
link = SocialLink(
|
||||
platform_name=data['platform_name'],
|
||||
url=data['url'],
|
||||
icon_class=data['icon_class'],
|
||||
display_order=data.get('display_order', 0),
|
||||
animation_delay=data.get('animation_delay', '0s'),
|
||||
is_active=data.get('is_active', True)
|
||||
)
|
||||
db.session.add(link)
|
||||
db.session.commit()
|
||||
return jsonify(link.to_dict()), 201
|
||||
|
||||
|
||||
@route_api.route('/social-links/<int:link_id>', methods=['PUT'])
|
||||
def update_social_link(link_id):
|
||||
"""Update a social link"""
|
||||
link = SocialLink.query.get_or_404(link_id)
|
||||
data = request.json
|
||||
|
||||
link.platform_name = data.get('platform_name', link.platform_name)
|
||||
link.url = data.get('url', link.url)
|
||||
link.icon_class = data.get('icon_class', link.icon_class)
|
||||
link.display_order = data.get('display_order', link.display_order)
|
||||
link.animation_delay = data.get('animation_delay', link.animation_delay)
|
||||
link.is_active = data.get('is_active', link.is_active)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(link.to_dict())
|
||||
|
||||
|
||||
@route_api.route('/social-links/<int:link_id>', methods=['DELETE'])
|
||||
def delete_social_link(link_id):
|
||||
"""Delete a social link"""
|
||||
link = SocialLink.query.get_or_404(link_id)
|
||||
db.session.delete(link)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Social link deleted successfully'})
|
||||
@@ -3,10 +3,24 @@
|
||||
|
||||
# Copyright Hersel Giannella
|
||||
|
||||
from quart import Blueprint, render_template
|
||||
from flask import Blueprint, render_template
|
||||
from models import Profile, Skill, Project, SocialLink
|
||||
|
||||
route_home = Blueprint('route_home', __name__)
|
||||
|
||||
@route_home.route('/')
|
||||
async def home():
|
||||
return await render_template('index.html')
|
||||
def home():
|
||||
"""Render home page with dynamic data from database"""
|
||||
# Fetch all data from database
|
||||
profile = Profile.query.first()
|
||||
skills = Skill.query.filter_by(is_active=True).order_by(Skill.display_order).all()
|
||||
projects = Project.query.filter_by(is_published=True).order_by(Project.display_order).all()
|
||||
social_links = SocialLink.query.filter_by(is_active=True).order_by(SocialLink.display_order).all()
|
||||
|
||||
return render_template(
|
||||
'index.html',
|
||||
profile=profile,
|
||||
skills=skills,
|
||||
projects=projects,
|
||||
social_links=social_links
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user