diff --git a/.env.example b/.env.example index 00f4127..673901b 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,12 @@ +# Flask Application Configuration APP_HOST=127.0.0.1 APP_PORT=5000 DEBUG=True -SECRET_KEY=yoursecretkey \ No newline at end of file +SECRET_KEY=change_this_to_a_random_secret_key + +# MariaDB Database Configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=portfolio_user +DB_PASSWORD=portfolio_password +DB_NAME=portfolio_db \ No newline at end of file diff --git a/README.md b/README.md index e414cf8..0e75309 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,183 @@ -# Python Server - Hersel.it +# Portfolio Dinamico - Hersel.it -Questo progetto รจ un'applicazione web sviluppata con **Quart** e configurata per essere eseguita tramite **Hypercorn** +Portfolio personale sviluppato con **Flask** e **MariaDB**, con gestione dinamica dei contenuti tramite API REST. -## Requisiti +## ๐Ÿš€ Caratteristiche + +- **Framework**: Flask (migrato da Quart per semplificare l'architettura) +- **Database**: MariaDB per la gestione dinamica dei contenuti +- **ORM**: SQLAlchemy con Flask-SQLAlchemy +- **API REST**: Endpoint per gestire progetti, competenze, profilo e social links +- **Docker**: Configurazione completa con Docker Compose +- **Responsive**: Design responsive con Bootstrap 5 + +## ๐Ÿ“‹ Requisiti - Python 3.10 o superiore +- MariaDB/MySQL 11.2 o superiore (o usa Docker Compose) - Pip (gestore dei pacchetti Python) -# Installazione +## ๐Ÿ”ง Installazione Locale -1. Clona il repository: - ```bash - git clone https://github.com/BluLupo/hersel.it.git - cd hersel.it - ``` +### 1. Clona il repository +```bash +git clone https://github.com/BluLupo/hersel.it.git +cd hersel.it +``` -2. Crea Ambiente Virtuale - ```bash - python3 -m venv env - ``` +### 2. Crea e attiva ambiente virtuale +```bash +python3 -m venv env +source env/bin/activate # Linux/Mac +# oppure +env\Scripts\activate # Windows +``` -3. Attiva Ambiente Virtuale - ```bash - source env/bin/activate - ``` +### 3. Installa le dipendenze +```bash +pip install -r requirements.txt +``` -4. Installa Le Dipendenze - ```bash - pip install -r requirements.txt - ``` - -# Configurazione -Modifica il file hypercorn_config.toml se necessario per adattarlo al tuo ambiente -Esempio di configurazione predefinita (hypercorn_config.toml): +### 4. Configura le variabili d'ambiente +```bash +cp .env.example .env +# Modifica .env con le tue credenziali del database +``` -```toml - bind = "0.0.0.0:5000" - workers = 1 - reload = true - ``` -# Avvio Applicazione - ```bash - hypercorn -c hypercorn_config.toml app:app - ``` +### 5. Configura MariaDB +Crea il database e l'utente: +```sql +CREATE DATABASE portfolio_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'portfolio_user'@'localhost' IDENTIFIED BY 'portfolio_password'; +GRANT ALL PRIVILEGES ON portfolio_db.* TO 'portfolio_user'@'localhost'; +FLUSH PRIVILEGES; +``` +### 6. Inizializza il database +```bash +python init_db.py +``` -# ๐Ÿš€ Avvio dell'applicazione con Docker -Questa applicazione utilizza Quart come web framework asincrono e Hypercorn come ASGI server +### 7. Avvia l'applicazione +```bash +# Modalitร  sviluppo +python app.py -โš™๏ธ Requisiti +# Modalitร  produzione con Gunicorn +gunicorn -w 4 -b 0.0.0.0:5000 app:app +``` + +## ๐Ÿณ Installazione con Docker + +### Requisiti - Docker - Docker Compose -# ๐Ÿ“„ Come avviare l'applicazione -1 - Crea un nuovo file docker-compose.yml nella tua macchina, con il seguente contenuto (oppure copialo direttamente da Qui ): - -```yml - version: "3.9" - -services: - quartapp: - image: python:3.10-slim - container_name: quartapp - working_dir: /app - ports: - - "127.0.0.1:5000:5000" - restart: always - command: > - sh -c " - apt-get update && - apt-get install -y git && - [ -d /app/.git ] || git clone https://github.com/BluLupo/hersel.it.git /app && - pip install --no-cache-dir -r requirements.txt && - hypercorn -c hypercorn_config.toml app:app - " - environment: - - PYTHONUNBUFFERED=1 - ``` - -2 - Esegui il servizio con Docker Compose: +### Avvio rapido ```bash - docker-compose up - ``` +docker-compose up -d +``` -# ๐Ÿ”— Accesso all'applicazione -Una volta avviata, l'applicazione sarร  accessibile da: +L'applicazione sarร  disponibile su `http://localhost:5000` + +Docker Compose avvierร  automaticamente: +- Container MariaDB sulla porta 3306 +- Container Flask sulla porta 5000 +- Inizializzazione automatica del database + +## ๐Ÿ“ Struttura del Progetto + +``` +hersel.it/ +โ”œโ”€โ”€ app.py # Applicazione Flask principale +โ”œโ”€โ”€ config.py # Configurazione +โ”œโ”€โ”€ models.py # Modelli SQLAlchemy +โ”œโ”€โ”€ init_db.py # Script inizializzazione database +โ”œโ”€โ”€ requirements.txt # Dipendenze Python +โ”œโ”€โ”€ docker-compose.yml # Configurazione Docker +โ”œโ”€โ”€ .env.example # Esempio variabili d'ambiente +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ home.py # Route homepage +โ”‚ โ””โ”€โ”€ api.py # API REST endpoints +โ”œโ”€โ”€ templates/ # Template Jinja2 +โ”‚ โ”œโ”€โ”€ index.html +โ”‚ โ”œโ”€โ”€ head.html +โ”‚ โ”œโ”€โ”€ navbar.html +โ”‚ โ””โ”€โ”€ content/ +โ”‚ โ”œโ”€โ”€ about.html +โ”‚ โ”œโ”€โ”€ project.html +โ”‚ โ””โ”€โ”€ links.html +โ””โ”€โ”€ static/ # File statici (CSS, JS, immagini) +``` + +## ๐Ÿ”Œ API REST Endpoints + +### Profile +- `GET /api/profile` - Ottieni informazioni profilo +- `PUT /api/profile` - Aggiorna profilo + +### Skills +- `GET /api/skills` - Lista competenze +- `POST /api/skills` - Crea competenza +- `PUT /api/skills/` - Aggiorna competenza +- `DELETE /api/skills/` - Elimina competenza + +### Projects +- `GET /api/projects` - Lista progetti +- `POST /api/projects` - Crea progetto +- `PUT /api/projects/` - Aggiorna progetto +- `DELETE /api/projects/` - Elimina progetto + +### Social Links +- `GET /api/social-links` - Lista link social +- `POST /api/social-links` - Crea link social +- `PUT /api/social-links/` - Aggiorna link social +- `DELETE /api/social-links/` - Elimina link social + +## ๐Ÿ“Š Schema Database + +### Tabelle +- `profile` - Informazioni personali +- `skills` - Competenze tecnologiche +- `projects` - Portfolio progetti +- `project_tags` - Tag/badge progetti +- `social_links` - Link profili social + +## ๐Ÿ”„ Migrazione da Quart a Flask + +Questo progetto รจ stato migrato da Quart (framework asincrono) a Flask (framework sincrono) per: +- Semplificare l'architettura +- Migliorare la compatibilitร  con librerie esistenti +- Facilitare il deployment con server WSGI standard (Gunicorn) +- Ridurre la complessitร  per un portfolio che non richiede operazioni async intensive + +## ๐Ÿ› ๏ธ Sviluppo + +### Aggiungere un nuovo progetto via API ```bash - http://127.0.0.1:5000 - ``` +curl -X POST http://localhost:5000/api/projects \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Nuovo Progetto", + "description": "Descrizione del progetto", + "image_url": "img/project.webp", + "github_url": "https://github.com/username/repo", + "tags": [ + {"name": "Python", "color_class": "bg-primary"}, + {"name": "Flask", "color_class": "bg-info"} + ] + }' +``` + +## ๐Ÿ“ Licenza + +Copyright Hersel Giannella + +## ๐Ÿ”— Link Utili + +- Portfolio Live: [https://hersel.it](https://hersel.it) +- GitHub: [https://github.com/BluLupo](https://github.com/BluLupo) +- LinkedIn: [https://linkedin.com/in/hersel](https://linkedin.com/in/hersel) diff --git a/app.py b/app.py index bef86b8..f653f1a 100644 --- a/app.py +++ b/app.py @@ -3,31 +3,43 @@ # Copyright Hersel Giannella -from quart import Quart, send_from_directory +from flask import Flask, send_from_directory from config import config +from models import db from routes.home import route_home +from routes.api import route_api -app = Quart( +app = Flask( __name__, template_folder="templates", static_folder="static", ) +# Load configuration +app.config['SECRET_KEY'] = config.SECRET_KEY +app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = config.SQLALCHEMY_TRACK_MODIFICATIONS +app.config['SQLALCHEMY_ECHO'] = config.SQLALCHEMY_ECHO + +# Initialize database +db.init_app(app) + # favicon.ico, sitemap.xml and robots.txt @app.route('/favicon.ico') -async def favicon(): - return await send_from_directory(app.static_folder, 'favicon.ico') +def favicon(): + return send_from_directory(app.static_folder, 'favicon.ico') @app.route('/sitemap.xml') -async def sitemap(): - return await send_from_directory(app.static_folder, 'sitemap.xml') +def sitemap(): + return send_from_directory(app.static_folder, 'sitemap.xml') @app.route('/robots.txt') -async def robots(): - return await send_from_directory(app.static_folder, 'robots.txt') +def robots(): + return send_from_directory(app.static_folder, 'robots.txt') # BluePrint Routes app.register_blueprint(route_home) +app.register_blueprint(route_api) if __name__ == '__main__': app.run(debug=config.DEBUG, host=config.APP_HOST, port=config.APP_PORT) diff --git a/config.py b/config.py index a577a0e..6329c64 100644 --- a/config.py +++ b/config.py @@ -11,6 +11,21 @@ class Config(BaseSettings): DEBUG: bool = True SECRET_KEY: str = "default_secret_key" + # Database Configuration + DB_HOST: str = "localhost" + DB_PORT: int = 3306 + DB_USER: str = "portfolio_user" + DB_PASSWORD: str = "portfolio_password" + DB_NAME: str = "portfolio_db" + + @property + def SQLALCHEMY_DATABASE_URI(self) -> str: + """Construct MariaDB connection string""" + return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}" + + SQLALCHEMY_TRACK_MODIFICATIONS: bool = False + SQLALCHEMY_ECHO: bool = False # Set to True for SQL query debugging + class Config: env_file = ".env" diff --git a/docker-compose.yml b/docker-compose.yml index 2c923b5..0adeea4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,51 @@ version: "3.9" services: - quartapp: + mariadb: + image: mariadb:11.2 + container_name: portfolio_mariadb + restart: always + environment: + MYSQL_ROOT_PASSWORD: root_password_change_me + MYSQL_DATABASE: portfolio_db + MYSQL_USER: portfolio_user + MYSQL_PASSWORD: portfolio_password + volumes: + - mariadb_data:/var/lib/mysql + ports: + - "127.0.0.1:3306:3306" + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + + flaskapp: image: python:3.10-slim - container_name: quartapp + container_name: portfolio_flaskapp working_dir: /app ports: - "127.0.0.1:5000:5000" restart: always + depends_on: + mariadb: + condition: service_healthy command: > sh -c " apt-get update && - apt-get install -y git && + apt-get install -y git default-libmysqlclient-dev build-essential pkg-config && [ -d /app/.git ] || git clone https://github.com/BluLupo/hersel.it.git /app && pip install --no-cache-dir -r requirements.txt && - hypercorn -c hypercorn_config.toml app:app + python init_db.py && + gunicorn -w 4 -b 0.0.0.0:5000 app:app " environment: - PYTHONUNBUFFERED=1 + - DB_HOST=mariadb + - DB_PORT=3306 + - DB_USER=portfolio_user + - DB_PASSWORD=portfolio_password + - DB_NAME=portfolio_db + +volumes: + mariadb_data: diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..840b8d1 --- /dev/null +++ b/init_db.py @@ -0,0 +1,180 @@ +""" +Database initialization script +Populates the database with initial portfolio data +""" +from app import app +from models import db, Profile, Skill, Project, ProjectTag, SocialLink + + +def init_database(): + """Initialize database with portfolio data""" + with app.app_context(): + # Drop all tables and recreate + print("Dropping all tables...") + db.drop_all() + + print("Creating all tables...") + db.create_all() + + # Create profile information + print("Adding profile information...") + profile = Profile( + title="Il ponte tra sistemi e sviluppo web", + lead_text="Con oltre 7 Anni di esperienza nello sviluppo di applicazioni web con Python Flask, offro soluzioni complete end-to-end.", + description_1="La mia doppia specializzazione mi permette di comprendere a fondo l'intero ciclo di vita delle applicazioni, dall'architettura del server fino all'implementazione e al deployment.", + description_2="Mi piace risolvere problemi complessi e creare soluzioni che siano robuste, scalabili e facili da mantenere.", + years_experience=7 + ) + db.session.add(profile) + + # Create skills + print("Adding skills...") + skills_data = [ + {"name": "Linux", "icon_class": "fab fa-linux", "category": "OS", "display_order": 1}, + {"name": "Windows", "icon_class": "fab fa-windows", "category": "OS", "display_order": 2}, + {"name": "Python", "icon_class": "fab fa-python", "category": "Language", "display_order": 3}, + {"name": "Flask", "icon_class": "fas fa-flask", "category": "Framework", "display_order": 4}, + {"name": "Database", "icon_class": "fas fa-database", "category": "Tool", "display_order": 5}, + {"name": "Docker", "icon_class": "fab fa-docker", "category": "Tool", "display_order": 6}, + {"name": "Server", "icon_class": "fas fa-server", "category": "Infrastructure", "display_order": 7}, + {"name": "Networking", "icon_class": "fas fa-network-wired", "category": "Infrastructure", "display_order": 8}, + ] + + for skill_data in skills_data: + skill = Skill(**skill_data) + db.session.add(skill) + + # Create projects + print("Adding projects...") + + # Project 1: Database Backup Script + project1 = Project( + title="Script di Backup Database (MariaDB/MySQL)", + description="Script in Bash per sistemi Linux che permette l'automazione dei backup database", + image_url="img/bash.webp", + github_url="https://github.com/BluLupo/server-script/tree/main/db_bash_backup-main", + display_order=1, + animation_delay="0s" + ) + db.session.add(project1) + db.session.flush() # Get project1.id + + # Project 1 tags + project1_tags = [ + ProjectTag(project_id=project1.id, name="Bash", color_class="bg-primary", display_order=1), + ProjectTag(project_id=project1.id, name="Linux", color_class="bg-info", display_order=2), + ] + db.session.add_all(project1_tags) + + # Project 2: ByteStash + project2 = Project( + title="Personal ByteStash", + description="Ho realizzato un repository personale di snippet sfruttando Bytestash, ottimizzando la gestione del codice riutilizzabile e migliorando la produttivitร  nello sviluppo di progetti software.", + image_url="img/byte.webp", + demo_url="https://bytestash.gwserver.it/public/snippets", + display_order=2, + animation_delay="0.2s" + ) + db.session.add(project2) + db.session.flush() + + # Project 2 tags + project2_tags = [ + ProjectTag(project_id=project2.id, name="LXC", color_class="bg-warning text-dark", display_order=1), + ProjectTag(project_id=project2.id, name="Proxmox", color_class="bg-dark", display_order=2), + ProjectTag(project_id=project2.id, name="Nginx", color_class="bg-info", display_order=3), + ProjectTag(project_id=project2.id, name="Reverse Proxy", color_class="bg-secondary", display_order=4), + ProjectTag(project_id=project2.id, name="Linux", color_class="bg-primary", display_order=5), + ProjectTag(project_id=project2.id, name="Self-hosted", color_class="bg-primary", display_order=6), + ] + db.session.add_all(project2_tags) + + # Project 3: Nextcloud + project3 = Project( + title="Nextcloud Personale", + description="Installazione di Nextcloud su container LXC con database PostgreSQL e caching Redis, integrato in una rete privata con gestione IP tramite server DHCP.", + image_url="img/next.webp", + demo_url="https://cloud.gwserver.it", + display_order=3, + animation_delay="0.4s" + ) + db.session.add(project3) + db.session.flush() + + # Project 3 tags + project3_tags = [ + ProjectTag(project_id=project3.id, name="Nextcloud", color_class="bg-primary", display_order=1), + ProjectTag(project_id=project3.id, name="PostgreSQL", color_class="bg-secondary", display_order=2), + ProjectTag(project_id=project3.id, name="Redis", color_class="bg-info", display_order=3), + ProjectTag(project_id=project3.id, name="LXC", color_class="bg-warning text-dark", display_order=4), + ProjectTag(project_id=project3.id, name="Proxmox", color_class="bg-dark", display_order=5), + ProjectTag(project_id=project3.id, name="Rete Privata", color_class="bg-success", display_order=6), + ProjectTag(project_id=project3.id, name="DHCP Server", color_class="bg-secondary", display_order=7), + ] + db.session.add_all(project3_tags) + + # Create social links + print("Adding social links...") + social_links_data = [ + { + "platform_name": "LinkedIn", + "url": "https://linkedin.com/in/hersel", + "icon_class": "fab fa-linkedin", + "display_order": 1, + "animation_delay": "0.1s" + }, + { + "platform_name": "GitHub", + "url": "https://github.com/blulupo", + "icon_class": "fab fa-github", + "display_order": 2, + "animation_delay": "0.2s" + }, + { + "platform_name": "Stack Overflow", + "url": "https://stackoverflow.com/users/11765177/hersel-giannella", + "icon_class": "fab fa-stack-overflow", + "display_order": 3, + "animation_delay": "0.3s" + }, + { + "platform_name": "CodeWars", + "url": "https://www.codewars.com/users/BluLupo", + "icon_class": "fas fa-code", + "display_order": 4, + "animation_delay": "0.4s" + }, + { + "platform_name": "Blog", + "url": "https://blog.hersel.it", + "icon_class": "fas fa-blog", + "display_order": 5, + "animation_delay": "0.5s" + }, + { + "platform_name": "Email", + "url": "mailto:info@hersel.it", + "icon_class": "fas fa-envelope", + "display_order": 6, + "animation_delay": "0.6s" + }, + ] + + for link_data in social_links_data: + social_link = SocialLink(**link_data) + db.session.add(social_link) + + # Commit all changes + print("Committing changes to database...") + db.session.commit() + + print("\nโœ… Database initialized successfully!") + print(f" - Profile: 1 record") + print(f" - Skills: {len(skills_data)} records") + print(f" - Projects: 3 records") + print(f" - Project Tags: {len(project1_tags) + len(project2_tags) + len(project3_tags)} records") + print(f" - Social Links: {len(social_links_data)} records") + + +if __name__ == '__main__': + init_database() diff --git a/models.py b/models.py new file mode 100644 index 0000000..af3954d --- /dev/null +++ b/models.py @@ -0,0 +1,133 @@ +""" +Database models for Portfolio Application +Uses SQLAlchemy ORM with MariaDB +""" +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +db = SQLAlchemy() + + +class Profile(db.Model): + """Store personal profile information""" + __tablename__ = 'profile' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(255), nullable=False) + lead_text = db.Column(db.Text, nullable=False) + description_1 = db.Column(db.Text) + description_2 = db.Column(db.Text) + years_experience = db.Column(db.Integer, default=7) + cv_url = db.Column(db.String(500)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def to_dict(self): + return { + 'id': self.id, + 'title': self.title, + 'lead_text': self.lead_text, + 'description_1': self.description_1, + 'description_2': self.description_2, + 'years_experience': self.years_experience, + 'cv_url': self.cv_url + } + + +class Skill(db.Model): + """Store technical skills/technologies""" + __tablename__ = 'skills' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + icon_class = db.Column(db.String(100), nullable=False) + category = db.Column(db.String(50)) # OS, Language, Framework, Tool, etc. + proficiency_level = db.Column(db.Integer) # 1-5 (optional) + display_order = db.Column(db.Integer, default=0) + is_active = db.Column(db.Boolean, default=True) + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'icon_class': self.icon_class, + 'category': self.category, + 'proficiency_level': self.proficiency_level, + 'display_order': self.display_order + } + + +class Project(db.Model): + """Store portfolio projects""" + __tablename__ = 'projects' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(255), nullable=False) + description = db.Column(db.Text, nullable=False) + image_url = db.Column(db.String(500)) + demo_url = db.Column(db.String(500)) + github_url = db.Column(db.String(500)) + display_order = db.Column(db.Integer, default=0) + animation_delay = db.Column(db.String(10), default='0s') # e.g., '0.2s' + is_published = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationship + tags = db.relationship('ProjectTag', backref='project', lazy=True, cascade='all, delete-orphan') + + def to_dict(self): + return { + 'id': self.id, + 'title': self.title, + 'description': self.description, + 'image_url': self.image_url, + 'demo_url': self.demo_url, + 'github_url': self.github_url, + 'display_order': self.display_order, + 'animation_delay': self.animation_delay, + 'is_published': self.is_published, + 'tags': [tag.to_dict() for tag in self.tags] + } + + +class ProjectTag(db.Model): + """Store tags/badges for projects""" + __tablename__ = 'project_tags' + + id = db.Column(db.Integer, primary_key=True) + project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False) + name = db.Column(db.String(50), nullable=False) + color_class = db.Column(db.String(50), default='bg-primary') # Bootstrap badge classes + display_order = db.Column(db.Integer, default=0) + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'color_class': self.color_class, + 'display_order': self.display_order + } + + +class SocialLink(db.Model): + """Store social media and profile links""" + __tablename__ = 'social_links' + + id = db.Column(db.Integer, primary_key=True) + platform_name = db.Column(db.String(100), nullable=False) + url = db.Column(db.String(500), nullable=False) + icon_class = db.Column(db.String(100), nullable=False) + display_order = db.Column(db.Integer, default=0) + animation_delay = db.Column(db.String(10), default='0s') # e.g., '0.1s' + is_active = db.Column(db.Boolean, default=True) + + def to_dict(self): + return { + 'id': self.id, + 'platform_name': self.platform_name, + 'url': self.url, + 'icon_class': self.icon_class, + 'display_order': self.display_order, + 'animation_delay': self.animation_delay + } diff --git a/requirements.txt b/requirements.txt index e3e176b..158e2ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,32 @@ -aiofiles==24.1.0 -annotated-types==0.7.0 -blinker==1.9.0 -click==8.1.8 +# Core Flask Framework Flask==3.1.0 -h11==0.14.0 -h2==4.1.0 -hpack==4.0.0 -Hypercorn==0.17.3 -hyperframe==6.0.1 -itsdangerous==2.2.0 Jinja2==3.1.5 +Werkzeug==3.1.3 +click==8.1.8 +itsdangerous==2.2.0 MarkupSafe==3.0.2 -priority==2.0.0 +blinker==1.9.0 + +# Database - Flask-SQLAlchemy and MariaDB/MySQL driver +Flask-SQLAlchemy==3.1.1 +SQLAlchemy==2.0.36 +PyMySQL==1.1.1 +cryptography==44.0.0 + +# Configuration Management pydantic==2.10.4 pydantic-settings==2.7.1 pydantic_core==2.27.2 python-dotenv==1.0.1 -Quart==0.20.0 +annotated-types==0.7.0 typing_extensions==4.12.2 -Werkzeug==3.1.3 -wsproto==1.2.0 + +# Testing httpx==0.27.0 +pytest==8.3.4 + +# Documentation Sphinx==8.2.3 + +# WSGI Server (Production alternative to Flask dev server) +gunicorn==23.0.0 diff --git a/routes/api.py b/routes/api.py new file mode 100644 index 0000000..be21ceb --- /dev/null +++ b/routes/api.py @@ -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/', 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/', 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/', 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/', 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/', 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/', 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'}) diff --git a/routes/home.py b/routes/home.py index f69d595..62c2daf 100644 --- a/routes/home.py +++ b/routes/home.py @@ -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 + ) diff --git a/templates/content/about.html b/templates/content/about.html index 47e62f6..d53aa3f 100644 --- a/templates/content/about.html +++ b/templates/content/about.html @@ -3,52 +3,77 @@

Chi Sono

+ {% if profile %} +

{{ profile.title }}

+

{{ profile.lead_text }}

+ {% if profile.description_1 %} +

{{ profile.description_1 }}

+ {% endif %} + {% if profile.description_2 %} +

{{ profile.description_2 }}

+ {% endif %} +
+ {% if profile.cv_url %} + Scarica CV + {% endif %} + I Miei Profili +
+ {% else %}

Il ponte tra sistemi e sviluppo web

Con oltre 7 Anni di esperienza nello sviluppo di applicazioni web con Python Flask, offro soluzioni complete end-to-end.

La mia doppia specializzazione mi permette di comprendere a fondo l'intero ciclo di vita delle applicazioni, dall'architettura del server fino all'implementazione e al deployment.

Mi piace risolvere problemi complessi e creare soluzioni che siano robuste, scalabili e facili da mantenere.

+ {% endif %}

Tecnologie che utilizzo

-
- -

Linux

-
-
- -

Windows

-
-
- -

Python

-
-
- -

Flask

-
-
- -

Database

-
-
- -

Docker

-
-
- -

Server

-
-
- -

Networking

-
+ {% if skills %} + {% for skill in skills %} +
+ +

{{ skill.name }}

+
+ {% endfor %} + {% else %} +
+ +

Linux

+
+
+ +

Windows

+
+
+ +

Python

+
+
+ +

Flask

+
+
+ +

Database

+
+
+ +

Docker

+
+
+ +

Server

+
+
+ +

Networking

+
+ {% endif %}
diff --git a/templates/content/links.html b/templates/content/links.html index 0cfc999..e43a473 100644 --- a/templates/content/links.html +++ b/templates/content/links.html @@ -6,42 +6,53 @@

Scopri di piรน sul mio lavoro e segui i miei progetti attraverso questi canali

diff --git a/templates/content/project.html b/templates/content/project.html index d5b4206..1f866a2 100644 --- a/templates/content/project.html +++ b/templates/content/project.html @@ -2,68 +2,95 @@

I Miei Progetti

- -
-
- Progetto 1 -
-
Script di Backup Database (MariaDB/MySQL)
-

Script in Bash per sistemi Linux che permette l'automazione dei backup database

-
-
- Bash - Linux + {% if projects %} + {% for project in projects %} + +
+
+ {% if project.image_url %} + {{ project.title }} + {% endif %} +
+
{{ project.title }}
+

{{ project.description }}

+
+
+ {% for tag in project.tags|sort(attribute='display_order') %} + {{ tag.name }} + {% endfor %} +
+ {% if project.github_url or project.demo_url %} + Dettagli + {% endif %} +
+
+
+
+ {% endfor %} + {% else %} + +
+
+ Progetto 1 +
+
Script di Backup Database (MariaDB/MySQL)
+

Script in Bash per sistemi Linux che permette l'automazione dei backup database

+
+
+ Bash + Linux +
+ Dettagli
- Dettagli
-
- -
-
- Progetto 2 -
-
Personal ByteStash
-

Ho realizzato un repository personale di snippet sfruttando Bytestash, ottimizzando la gestione del codice riutilizzabile e migliorando la produttivitร  nello sviluppo di progetti software.

-
-
- LXC - Proxmox - Nginx - Reverse Proxy - Linux - Self-hosted + +
+
+ Progetto 2 +
+
Personal ByteStash
+

Ho realizzato un repository personale di snippet sfruttando Bytestash, ottimizzando la gestione del codice riutilizzabile e migliorando la produttivitร  nello sviluppo di progetti software.

+
+
+ LXC + Proxmox + Nginx + Reverse Proxy + Linux + Self-hosted +
+ Dettagli
- Dettagli
-
- -
-
- Nextcloud Personale -
-
Nextcloud Personale
-

Installazione di Nextcloud su container LXC con database PostgreSQL e caching Redis, integrato in una rete privata con gestione IP tramite server DHCP.

-
-
- Nextcloud - PostgreSQL - Redis - LXC - Proxmox - Rete Privata - DHCP Server + +
+
+ Nextcloud Personale +
+
Nextcloud Personale
+

Installazione di Nextcloud su container LXC con database PostgreSQL e caching Redis, integrato in una rete privata con gestione IP tramite server DHCP.

+
+
+ Nextcloud + PostgreSQL + Redis + LXC + Proxmox + Rete Privata + DHCP Server +
+ Dettagli
- Dettagli
-
+ {% endif %}