Initial clean commit
This commit is contained in:
commit
6784d81c2c
141 changed files with 25219 additions and 0 deletions
267
routers/account.py
Normal file
267
routers/account.py
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
"""
|
||||
Account management router - User profile and account settings.
|
||||
"""
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from psycopg2 import extras
|
||||
from db import get_conn
|
||||
from utils.auth import get_current_user, login_required, hash_password, verify_password, validate_password
|
||||
from datetime import datetime
|
||||
|
||||
account_bp = Blueprint("account", __name__, url_prefix="/account")
|
||||
|
||||
|
||||
@account_bp.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
"""User account dashboard."""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
with get_conn() as conn:
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
# Get favorites count
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) as count
|
||||
FROM favoritos
|
||||
WHERE user_id = %s
|
||||
""", (user['id'],))
|
||||
favorites_count = cur.fetchone()['count']
|
||||
|
||||
# Get search history count
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) as count
|
||||
FROM search_history
|
||||
WHERE user_id = %s
|
||||
""", (user['id'],))
|
||||
searches_count = cur.fetchone()['count']
|
||||
|
||||
# Get recent searches (last 10)
|
||||
cur.execute("""
|
||||
SELECT query, results_count, searched_at
|
||||
FROM search_history
|
||||
WHERE user_id = %s
|
||||
ORDER BY searched_at DESC
|
||||
LIMIT 10
|
||||
""", (user['id'],))
|
||||
recent_searches = cur.fetchall()
|
||||
|
||||
# Get recent favorites (last 5)
|
||||
cur.execute("""
|
||||
SELECT n.id, n.titulo, n.imagen_url, f.created_at,
|
||||
t.titulo_trad, t.id AS traduccion_id
|
||||
FROM favoritos f
|
||||
JOIN noticias n ON n.id = f.noticia_id
|
||||
LEFT JOIN traducciones t ON t.noticia_id = n.id AND t.lang_to = 'es' AND t.status = 'done'
|
||||
WHERE f.user_id = %s
|
||||
ORDER BY f.created_at DESC
|
||||
LIMIT 5
|
||||
""", (user['id'],))
|
||||
recent_favorites = cur.fetchall()
|
||||
|
||||
return render_template("account.html",
|
||||
user=user,
|
||||
favorites_count=favorites_count,
|
||||
searches_count=searches_count,
|
||||
recent_searches=recent_searches,
|
||||
recent_favorites=recent_favorites)
|
||||
|
||||
|
||||
@account_bp.route("/search-history")
|
||||
@login_required
|
||||
def search_history():
|
||||
"""Full search history page."""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
per_page = 50
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
with get_conn() as conn:
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
# Get total count
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) as count
|
||||
FROM search_history
|
||||
WHERE user_id = %s
|
||||
""", (user['id'],))
|
||||
total = cur.fetchone()['count']
|
||||
|
||||
# Get paginated results
|
||||
cur.execute("""
|
||||
SELECT query, results_count, searched_at
|
||||
FROM search_history
|
||||
WHERE user_id = %s
|
||||
ORDER BY searched_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""", (user['id'], per_page, offset))
|
||||
searches = cur.fetchall()
|
||||
|
||||
total_pages = (total + per_page - 1) // per_page
|
||||
|
||||
return render_template("search_history.html",
|
||||
user=user,
|
||||
searches=searches,
|
||||
page=page,
|
||||
total_pages=total_pages,
|
||||
total=total)
|
||||
|
||||
|
||||
@account_bp.route("/change-password", methods=["POST"])
|
||||
@login_required
|
||||
def change_password():
|
||||
"""Change user password."""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
current_password = request.form.get("current_password", "")
|
||||
new_password = request.form.get("new_password", "")
|
||||
new_password_confirm = request.form.get("new_password_confirm", "")
|
||||
|
||||
# Validation
|
||||
if not current_password or not new_password:
|
||||
flash("Por favor completa todos los campos", "danger")
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
valid_password, password_error = validate_password(new_password)
|
||||
if not valid_password:
|
||||
flash(password_error, "danger")
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
if new_password != new_password_confirm:
|
||||
flash("Las contraseñas nuevas no coinciden", "danger")
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
try:
|
||||
with get_conn() as conn:
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
# Verify current password
|
||||
cur.execute("""
|
||||
SELECT password_hash
|
||||
FROM usuarios
|
||||
WHERE id = %s
|
||||
""", (user['id'],))
|
||||
result = cur.fetchone()
|
||||
|
||||
if not result or not verify_password(current_password, result['password_hash']):
|
||||
flash("La contraseña actual es incorrecta", "danger")
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
# Update password
|
||||
new_hash = hash_password(new_password)
|
||||
cur.execute("""
|
||||
UPDATE usuarios
|
||||
SET password_hash = %s, updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (new_hash, user['id']))
|
||||
conn.commit()
|
||||
|
||||
flash("Contraseña actualizada exitosamente", "success")
|
||||
except Exception as e:
|
||||
flash("Error al actualizar la contraseña", "danger")
|
||||
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
|
||||
@account_bp.route("/upload-avatar", methods=["POST"])
|
||||
@login_required
|
||||
def upload_avatar():
|
||||
"""Upload user avatar."""
|
||||
import os
|
||||
import secrets
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import current_app
|
||||
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if 'avatar' not in request.files:
|
||||
flash("No se seleccionó ningún archivo", "danger")
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
file = request.files['avatar']
|
||||
if file.filename == '':
|
||||
flash("No se seleccionó ningún archivo", "danger")
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
if file:
|
||||
# Check extension
|
||||
allowed_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.webp'}
|
||||
_, ext = os.path.splitext(file.filename)
|
||||
if ext.lower() not in allowed_extensions:
|
||||
flash("Formato de imagen no permitido. Usa JPG, PNG, GIF o WEBP.", "danger")
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
# Save file
|
||||
try:
|
||||
# Create filename using user ID and random partial to avoid caching issues
|
||||
random_hex = secrets.token_hex(4)
|
||||
filename = f"user_{user['id']}_{random_hex}{ext.lower()}"
|
||||
|
||||
# Ensure upload folder exists
|
||||
upload_folder = os.path.join(current_app.root_path, 'static/uploads/avatars')
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
|
||||
# Delete old avatar if exists
|
||||
if user.get('avatar_url'):
|
||||
old_path = os.path.join(current_app.root_path, user['avatar_url'].lstrip('/'))
|
||||
if os.path.exists(old_path) and 'user_' in old_path: # Safety check
|
||||
try:
|
||||
os.remove(old_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
file_path = os.path.join(upload_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Update DB
|
||||
relative_path = f"/static/uploads/avatars/{filename}"
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
UPDATE usuarios
|
||||
SET avatar_url = %s, updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (relative_path, user['id']))
|
||||
conn.commit()
|
||||
|
||||
# Update session
|
||||
from flask import session
|
||||
session['avatar_url'] = relative_path
|
||||
|
||||
flash("Foto de perfil actualizada", "success")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error uploading avatar: {e}")
|
||||
flash("Error al subir la imagen", "danger")
|
||||
|
||||
return redirect(url_for('account.index'))
|
||||
|
||||
|
||||
@account_bp.route("/stats")
|
||||
@login_required
|
||||
def stats():
|
||||
"""Get user statistics as JSON."""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({"error": "Not authenticated"}), 401
|
||||
|
||||
with get_conn() as conn:
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
cur.execute("""
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM favoritos WHERE user_id = %s) as favorites_count,
|
||||
(SELECT COUNT(*) FROM search_history WHERE user_id = %s) as searches_count,
|
||||
(SELECT MAX(searched_at) FROM search_history WHERE user_id = %s) as last_search
|
||||
""", (user['id'], user['id'], user['id']))
|
||||
stats = cur.fetchone()
|
||||
|
||||
return jsonify({
|
||||
"favorites_count": stats['favorites_count'],
|
||||
"searches_count": stats['searches_count'],
|
||||
"last_search": stats['last_search'].isoformat() if stats['last_search'] else None
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue