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)
240 lines
8.0 KiB
Python
240 lines
8.0 KiB
Python
#!/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'})
|