Files
hersel.it/routes/admin.py
Claude 6845308a34 Add password change functionality and profile image upload
- Add profile_image field to Profile model with default value
- Update profile edit route to handle profile image file uploads
- Add password change route with validation in auth module
- Create change password template with form
- Update profile template to include image upload with preview
- Add password change link to admin sidebar
- Update homepage to use dynamic profile image from database
2025-11-13 15:58:51 +00:00

352 lines
13 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright Hersel Giannella
"""
Admin dashboard routes for managing portfolio content
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from models import db, Profile, Skill, Project, ProjectTag, SocialLink
import os
route_admin = Blueprint('admin', __name__, url_prefix='/admin')
def allowed_file(filename):
"""Check if file extension is allowed"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
def save_uploaded_file(file):
"""Save uploaded file and return the relative path"""
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# Add timestamp to avoid collisions
from datetime import datetime
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_')
filename = timestamp + filename
# Create upload folder if it doesn't exist
upload_folder = current_app.config['UPLOAD_FOLDER']
os.makedirs(upload_folder, exist_ok=True)
filepath = os.path.join(upload_folder, filename)
file.save(filepath)
# Return relative path from static folder
return f"img/{filename}"
return None
@route_admin.route('/')
@login_required
def dashboard():
"""Admin dashboard home"""
# Statistiche
stats = {
'projects': Project.query.count(),
'skills': Skill.query.count(),
'social_links': SocialLink.query.count(),
'published_projects': Project.query.filter_by(is_published=True).count()
}
return render_template('admin/dashboard.html', stats=stats)
# ============================================================================
# PROFILE MANAGEMENT
# ============================================================================
@route_admin.route('/profile')
@login_required
def profile_manage():
"""Manage profile information"""
profile = Profile.query.first()
return render_template('admin/profile.html', profile=profile)
@route_admin.route('/profile/edit', methods=['POST'])
@login_required
def profile_edit():
"""Edit profile information"""
profile = Profile.query.first()
if not profile:
flash('Profilo non trovato.', 'danger')
return redirect(url_for('admin.profile_manage'))
# Handle profile image upload
profile_image_url = request.form.get('profile_image', profile.profile_image)
if 'profile_image_file' in request.files:
file = request.files['profile_image_file']
if file.filename:
uploaded_path = save_uploaded_file(file)
if uploaded_path:
profile_image_url = uploaded_path
else:
flash('Formato immagine non valido. Usa: png, jpg, jpeg, gif, webp', 'danger')
return redirect(url_for('admin.profile_manage'))
profile.title = request.form.get('title', profile.title)
profile.lead_text = request.form.get('lead_text', profile.lead_text)
profile.description_1 = request.form.get('description_1', profile.description_1)
profile.description_2 = request.form.get('description_2', profile.description_2)
profile.years_experience = int(request.form.get('years_experience', profile.years_experience))
profile.cv_url = request.form.get('cv_url', profile.cv_url)
profile.profile_image = profile_image_url
db.session.commit()
flash('Profilo aggiornato con successo!', 'success')
return redirect(url_for('admin.profile_manage'))
# ============================================================================
# SKILLS MANAGEMENT
# ============================================================================
@route_admin.route('/skills')
@login_required
def skills_manage():
"""Manage skills"""
skills = Skill.query.order_by(Skill.display_order).all()
return render_template('admin/skills.html', skills=skills)
@route_admin.route('/skills/add', methods=['POST'])
@login_required
def skill_add():
"""Add new skill"""
skill = Skill(
name=request.form.get('name'),
icon_class=request.form.get('icon_class'),
category=request.form.get('category'),
display_order=int(request.form.get('display_order', 0)),
is_active=request.form.get('is_active') == 'on'
)
db.session.add(skill)
db.session.commit()
flash('Skill aggiunta con successo!', 'success')
return redirect(url_for('admin.skills_manage'))
@route_admin.route('/skills/<int:skill_id>/edit', methods=['POST'])
@login_required
def skill_edit(skill_id):
"""Edit skill"""
skill = Skill.query.get_or_404(skill_id)
skill.name = request.form.get('name', skill.name)
skill.icon_class = request.form.get('icon_class', skill.icon_class)
skill.category = request.form.get('category', skill.category)
skill.display_order = int(request.form.get('display_order', skill.display_order))
skill.is_active = request.form.get('is_active') == 'on'
db.session.commit()
flash('Skill aggiornata con successo!', 'success')
return redirect(url_for('admin.skills_manage'))
@route_admin.route('/skills/<int:skill_id>/delete', methods=['POST'])
@login_required
def skill_delete(skill_id):
"""Delete skill"""
skill = Skill.query.get_or_404(skill_id)
db.session.delete(skill)
db.session.commit()
flash('Skill eliminata con successo!', 'success')
return redirect(url_for('admin.skills_manage'))
# ============================================================================
# PROJECTS MANAGEMENT
# ============================================================================
@route_admin.route('/projects')
@login_required
def projects_manage():
"""Manage projects"""
projects = Project.query.order_by(Project.display_order).all()
return render_template('admin/projects.html', projects=projects)
@route_admin.route('/projects/add', methods=['GET', 'POST'])
@login_required
def project_add():
"""Add new project"""
if request.method == 'POST':
# Handle image upload
image_url = request.form.get('image_url') # Manual URL input
if 'image_file' in request.files:
file = request.files['image_file']
if file.filename: # Se è stato selezionato un file
uploaded_path = save_uploaded_file(file)
if uploaded_path:
image_url = uploaded_path
else:
flash('Formato immagine non valido. Usa: png, jpg, jpeg, gif, webp', 'danger')
return redirect(url_for('admin.project_add'))
project = Project(
title=request.form.get('title'),
description=request.form.get('description'),
image_url=image_url,
demo_url=request.form.get('demo_url'),
github_url=request.form.get('github_url'),
display_order=int(request.form.get('display_order', 0)),
animation_delay=request.form.get('animation_delay', '0s'),
is_published=request.form.get('is_published') == 'on'
)
db.session.add(project)
db.session.flush()
# Aggiungi tags
tags_input = request.form.get('tags', '')
if tags_input:
tags_list = [tag.strip() for tag in tags_input.split(',') if tag.strip()]
for idx, tag_name in enumerate(tags_list):
# Estrai colore se specificato (formato: "Python:bg-primary")
if ':' in tag_name:
tag_name, color = tag_name.split(':', 1)
else:
color = 'bg-primary'
tag = ProjectTag(
project_id=project.id,
name=tag_name.strip(),
color_class=color.strip(),
display_order=idx
)
db.session.add(tag)
db.session.commit()
flash('Progetto aggiunto con successo!', 'success')
return redirect(url_for('admin.projects_manage'))
return render_template('admin/project_form.html', project=None)
@route_admin.route('/projects/<int:project_id>/edit', methods=['GET', 'POST'])
@login_required
def project_edit(project_id):
"""Edit project"""
project = Project.query.get_or_404(project_id)
if request.method == 'POST':
# Handle image upload
image_url = request.form.get('image_url', project.image_url)
if 'image_file' in request.files:
file = request.files['image_file']
if file.filename: # Se è stato selezionato un file
uploaded_path = save_uploaded_file(file)
if uploaded_path:
image_url = uploaded_path
else:
flash('Formato immagine non valido. Usa: png, jpg, jpeg, gif, webp', 'danger')
return redirect(url_for('admin.project_edit', project_id=project_id))
project.title = request.form.get('title', project.title)
project.description = request.form.get('description', project.description)
project.image_url = image_url
project.demo_url = request.form.get('demo_url', project.demo_url)
project.github_url = request.form.get('github_url', project.github_url)
project.display_order = int(request.form.get('display_order', project.display_order))
project.animation_delay = request.form.get('animation_delay', project.animation_delay)
project.is_published = request.form.get('is_published') == 'on'
# Aggiorna tags
ProjectTag.query.filter_by(project_id=project.id).delete()
tags_input = request.form.get('tags', '')
if tags_input:
tags_list = [tag.strip() for tag in tags_input.split(',') if tag.strip()]
for idx, tag_name in enumerate(tags_list):
if ':' in tag_name:
tag_name, color = tag_name.split(':', 1)
else:
color = 'bg-primary'
tag = ProjectTag(
project_id=project.id,
name=tag_name.strip(),
color_class=color.strip(),
display_order=idx
)
db.session.add(tag)
db.session.commit()
flash('Progetto aggiornato con successo!', 'success')
return redirect(url_for('admin.projects_manage'))
return render_template('admin/project_form.html', project=project)
@route_admin.route('/projects/<int:project_id>/delete', methods=['POST'])
@login_required
def project_delete(project_id):
"""Delete project"""
project = Project.query.get_or_404(project_id)
db.session.delete(project)
db.session.commit()
flash('Progetto eliminato con successo!', 'success')
return redirect(url_for('admin.projects_manage'))
# ============================================================================
# SOCIAL LINKS MANAGEMENT
# ============================================================================
@route_admin.route('/social-links')
@login_required
def social_links_manage():
"""Manage social links"""
social_links = SocialLink.query.order_by(SocialLink.display_order).all()
return render_template('admin/social_links.html', social_links=social_links)
@route_admin.route('/social-links/add', methods=['POST'])
@login_required
def social_link_add():
"""Add new social link"""
link = SocialLink(
platform_name=request.form.get('platform_name'),
url=request.form.get('url'),
icon_class=request.form.get('icon_class'),
display_order=int(request.form.get('display_order', 0)),
animation_delay=request.form.get('animation_delay', '0s'),
is_active=request.form.get('is_active') == 'on'
)
db.session.add(link)
db.session.commit()
flash('Link social aggiunto con successo!', 'success')
return redirect(url_for('admin.social_links_manage'))
@route_admin.route('/social-links/<int:link_id>/edit', methods=['POST'])
@login_required
def social_link_edit(link_id):
"""Edit social link"""
link = SocialLink.query.get_or_404(link_id)
link.platform_name = request.form.get('platform_name', link.platform_name)
link.url = request.form.get('url', link.url)
link.icon_class = request.form.get('icon_class', link.icon_class)
link.display_order = int(request.form.get('display_order', link.display_order))
link.animation_delay = request.form.get('animation_delay', link.animation_delay)
link.is_active = request.form.get('is_active') == 'on'
db.session.commit()
flash('Link social aggiornato con successo!', 'success')
return redirect(url_for('admin.social_links_manage'))
@route_admin.route('/social-links/<int:link_id>/delete', methods=['POST'])
@login_required
def social_link_delete(link_id):
"""Delete social link"""
link = SocialLink.query.get_or_404(link_id)
db.session.delete(link)
db.session.commit()
flash('Link social eliminato con successo!', 'success')
return redirect(url_for('admin.social_links_manage'))