Aplicación para la gestión de proyectos Kanban con API REST, filtros avanzados y múltiples usuarios

Aplicación backend en Python para gestión Kanban multiusuario con API REST, filtros avanzados, autenticación JWT y manejo de proyectos, columnas y tareas.

Breve Introducción

Aplicación para la gestión de proyectos Kanban con API REST, filtros avanzados y múltiples usuarios (python)
DESCRIPCIÓN:
Una aplicación backend en Python que implementa un sistema Kanban multiusuario con autenticación, gestión de proyectos, columnas y tareas. Expone API REST completas con filtros avanzados para recuperar tareas según estado, asignados, fechas y prioridad. Utiliza Flask, SQLAlchemy y JWT para seguridad y persistencia.

NOTAS ADICIONALES:
La aplicación está lista para ejecutarse en un entorno Python con las dependencias instaladas. Puede extenderse con frontend o integrarse en sistemas más complejos. La seguridad se basa en JWT y hashing de contraseñas con passlib.

EXPLICACIONES:
- El modelo User gestiona los usuarios con contraseñas hasheadas y autenticación JWT.
- Project, Column y Task son entidades relacionadas: un proyecto contiene columnas, una columna contiene tareas.
- Las API REST permiten operaciones CRUD en todas las entidades con control de acceso por usuario.
- Los filtros avanzados para las tareas permiten buscar por estado, asignado, prioridad y rango de fechas.
- La configuración está centralizada en un archivo config.py para claves secretas y base de datos.
- La serialización y validación de solicitudes/respuestas se gestiona con Marshmallow.

Código de Ejemplo: python

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
from passlib.hash import pbkdf2_sha256 as sha256
from marshmallow import Schema, fields, ValidationError
from datetime import datetime, timedelta

# Configuración de la aplicación y base de datos
class Config:
    SECRET_KEY = 'super-secret-key-change-me'
    JWT_SECRET_KEY = 'jwt-super-secret-key-change-me'
    SQLALCHEMY_DATABASE_URI = 'sqlite:///kanban.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

app = Flask(__name__)
app.config.from_object(Config)

# Inicialización de extensiones
db = SQLAlchemy(app)
jwt = JWTManager(app)

# MODELOS DE BASE DE DATOS

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    projects = db.relationship('Project', backref='owner', lazy=True)

    @staticmethod
    def generate_hash(password):
        return sha256.hash(password)

    @staticmethod
    def verify_hash(password, hash_):
        return sha256.verify(password, hash_)

class Project(db.Model):
    __tablename__ = 'projects'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(120), nullable=False)
    description = db.Column(db.Text, nullable=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    columns = db.relationship('Column', backref='project', lazy=True, cascade='all, delete-orphan')

class Column(db.Model):
    __tablename__ = 'columns'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(120), nullable=False)
    project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False)
    tasks = db.relationship('Task', backref='column', lazy=True, cascade='all, delete-orphan')

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    description = db.Column(db.Text, nullable=True)
    status = db.Column(db.String(50), nullable=False)  # ej. 'todo', 'in-progress', 'done'
    priority = db.Column(db.Integer, default=3)  # 1=alta, 2=media, 3=baja
    due_date = db.Column(db.DateTime, nullable=True)
    assigned_to = db.Column(db.String(80), nullable=True)  # nombre de usuario asignado
    column_id = db.Column(db.Integer, db.ForeignKey('columns.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

# ESQUEMAS MARSHMALLOW PARA VALIDACIÓN Y SERIALIZACIÓN

class UserSchema(Schema):
    id = fields.Int(dump_only=True)
    username = fields.Str(required=True)
    password = fields.Str(load_only=True, required=True)

class ProjectSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True)
    description = fields.Str()
    user_id = fields.Int(dump_only=True)

class ColumnSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True)
    project_id = fields.Int(required=True)

class TaskSchema(Schema):
    id = fields.Int(dump_only=True)
    title = fields.Str(required=True)
    description = fields.Str()
    status = fields.Str(required=True)
    priority = fields.Int(validate=lambda x: 1 <= x <= 3)
    due_date = fields.DateTime(allow_none=True)
    assigned_to = fields.Str(allow_none=True)
    column_id = fields.Int(required=True)
    created_at = fields.DateTime(dump_only=True)
    updated_at = fields.DateTime(dump_only=True)

user_schema = UserSchema()
project_schema = ProjectSchema()
projects_schema = ProjectSchema(many=True)
column_schema = ColumnSchema()
columns_schema = ColumnSchema(many=True)
task_schema = TaskSchema()
tasks_schema = TaskSchema(many=True)

# RUTAS PARA AUTENTICACIÓN

@app.route('/register', methods=['POST'])
def register():
    # Registro de usuario con nombre de usuario y contraseña
    data = request.get_json()
    try:
        user_data = user_schema.load(data)
    except ValidationError as err:
        return jsonify(err.messages), 400

    if User.query.filter_by(username=user_data['username']).first():
        return jsonify({'message': 'Nombre de usuario ya existente'}), 400

    new_user = User(
        username=user_data['username'],
        password_hash=User.generate_hash(user_data['password'])
    )
    db.session.add(new_user)
    db.session.commit()

    return jsonify({'message': 'Usuario registrado con éxito'}), 201

@app.route('/login', methods=['POST'])
def login():
    # Login de usuario y generación de token JWT
    data = request.get_json()
    if not data or not data.get('username') or not data.get('password'):
        return jsonify({'message': 'Nombre de usuario y contraseña requeridos'}), 400

    user = User.query.filter_by(username=data['username']).first()
    if not user or not User.verify_hash(data['password'], user.password_hash):
        return jsonify({'message': 'Credenciales no válidas'}), 401

    access_token = create_access_token(identity=user.id, expires_delta=timedelta(hours=1))
    return jsonify({'access_token': access_token}), 200

# FUNCIONES DE APOYO

def get_current_user():
    user_id = get_jwt_identity()
    return User.query.get(user_id)

# RUTAS DE PROYECTOS

@app.route('/projects', methods=['POST'])
@jwt_required()
def create_project():
    user = get_current_user()
    data = request.get_json()
    try:
        project_data = project_schema.load(data)
    except ValidationError as err:
        return jsonify(err.messages), 400

    new_project = Project(
        name=project_data['name'],
        description=project_data.get('description'),
        owner=user
    )
    db.session.add(new_project)
    db.session.commit()

    return project_schema.jsonify(new_project), 201

@app.route('/projects', methods=['GET'])
@jwt_required()
def get_projects():
    user = get_current_user()
    projects = Project.query.filter_by(user_id=user.id).all()
    return projects_schema.jsonify(projects), 200

@app.route('/projects/<int:project_id>', methods=['GET'])
@jwt_required()
def get_project(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404
    return project_schema.jsonify(project), 200

@app.route('/projects/<int:project_id>', methods=['PUT'])
@jwt_required()
def update_project(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404

    data = request.get_json()
    try:
        project_data = project_schema.load(data, partial=True)
    except ValidationError as err:
        return jsonify(err.messages), 400

    if 'name' in project_data:
        project.name = project_data['name']
    if 'description' in project_data:
        project.description = project_data['description']

    db.session.commit()
    return project_schema.jsonify(project), 200

@app.route('/projects/<int:project_id>', methods=['DELETE'])
@jwt_required()
def delete_project(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404

    db.session.delete(project)
    db.session.commit()
    return jsonify({'message': 'Proyecto eliminado'}), 200

# RUTAS DE COLUMNAS

@app.route('/projects/<int:project_id>/columns', methods=['POST'])
@jwt_required()
def create_column(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404

    data = request.get_json()
    try:
        column_data = column_schema.load({**data, 'project_id': project_id})
    except ValidationError as err:
        return jsonify(err.messages), 400

    new_column = Column(name=column_data['name'], project=project)
    db.session.add(new_column)
    db.session.commit()

    return column_schema.jsonify(new_column), 201

@app.route('/projects/<int:project_id>/columns', methods=['GET'])
@jwt_required()
def get_columns(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404

    columns = Column.query.filter_by(project_id=project_id).all()
    return columns_schema.jsonify(columns), 200

@app.route('/columns/<int:column_id>', methods=['PUT'])
@jwt_required()
def update_column(column_id):
    user = get_current_user()
    column = Column.query.get(column_id)
    if not column or column.project.user_id != user.id:
        return jsonify({'message': 'Columna no encontrada o acceso denegado'}), 404

    data = request.get_json()
    try:
        column_data = column_schema.load(data, partial=True)
    except ValidationError as err:
        return jsonify(err.messages), 400

    if 'name' in column_data:
        column.name = column_data['name']

    db.session.commit()
    return column_schema.jsonify(column), 200

@app.route('/columns/<int:column_id>', methods=['DELETE'])
@jwt_required()
def delete_column(column_id):
    user = get_current_user()
    column = Column.query.get(column_id)
    if not column or column.project.user_id != user.id:
        return jsonify({'message': 'Columna no encontrada o acceso denegado'}), 404

    db.session.delete(column)
    db.session.commit()
    return jsonify({'message': 'Columna eliminada'}), 200

# RUTAS DE TAREAS

@app.route('/columns/<int:column_id>/tasks', methods=['POST'])
@jwt_required()
def create_task(column_id):
    user = get_current_user()
    column = Column.query.get(column_id)
    if not column or column.project.user_id != user.id:
        return jsonify({'message': 'Columna no encontrada o acceso denegado'}), 404

    data = request.get_json()
    try:
        task_data = task_schema.load({**data, 'column_id': column_id})
    except ValidationError as err:
        return jsonify(err.messages), 400

    new_task = Task(
        title=task_data['title'],
        description=task_data.get('description'),
        status=task_data['status'],
        priority=task_data.get('priority', 3),
        due_date=task_data.get('due_date'),
        assigned_to=task_data.get('assigned_to'),
        column=column
    )
    db.session.add(new_task)
    db.session.commit()

    return task_schema.jsonify(new_task), 201

@app.route('/columns/<int:column_id>/tasks', methods=['GET'])
@jwt_required()
def get_tasks(column_id):
    user = get_current_user()
    column = Column.query.get(column_id)
    if not column or column.project.user_id != user.id:
        return jsonify({'message': 'Columna no encontrada o acceso denegado'}), 404

    # Filtros avanzados
    status = request.args.get('status')
    assigned_to = request.args.get('assigned_to')
    priority = request.args.get('priority', type=int)
    due_date_from = request.args.get('due_date_from')
    due_date_to = request.args.get('due_date_to')

    query = Task.query.filter_by(column_id=column_id)

    if status:
        query = query.filter(Task.status == status)
    if assigned_to:
        query = query.filter(Task.assigned_to == assigned_to)
    if priority:
        query = query.filter(Task.priority == priority)
    if due_date_from:
        try:
            dt_from = datetime.fromisoformat(due_date_from)
            query = query.filter(Task.due_date >= dt_from)
        except ValueError:
            return jsonify({'message': 'Formato de fecha due_date_from no válido'}), 400
    if due_date_to:
        try:
            dt_to = datetime.fromisoformat(due_date_to)
            query = query.filter(Task.due_date <= dt_to)
        except ValueError:
            return jsonify({'message': 'Formato de fecha due_date_to no válido'}), 400

    tasks = query.all()
    return tasks_schema.jsonify(tasks), 200

@app.route('/tasks/<int:task_id>', methods=['GET'])
@jwt_required()
def get_task(task_id):
    user = get_current_user()
    task = Task.query.get(task_id)
    if not task or task.column.project.user_id != user.id:
        return jsonify({'message': 'Tarea no encontrada o acceso denegado'}), 404
    return task_schema.jsonify(task), 200

@app.route('/tasks/<int:task_id>', methods=['PUT'])
@jwt_required()
def update_task(task_id):
    user = get_current_user()
    task = Task.query.get(task_id)
    if not task or task.column.project.user_id != user.id:
        return jsonify({'message': 'Tarea no encontrada o acceso denegado'}), 404

    data = request.get_json()
    try:
        task_data = task_schema.load(data, partial=True)
    except ValidationError as err:
        return jsonify(err.messages), 400

    for key, value in task_data.items():
        setattr(task, key, value)

    db.session.commit()
    return task_schema.jsonify(task), 200

@app.route('/tasks/<int:task_id>', methods=['DELETE'])
@jwt_required()
def delete_task(task_id):
    user = get_current_user()
    task = Task.query.get(task_id)
    if not task or task.column.project.user_id != user.id:
        return jsonify({'message': 'Tarea no encontrada o acceso denegado'}), 404

    db.session.delete(task)
    db.session.commit()
    return jsonify({'message': 'Tarea eliminada'}), 200

# INICIO DE LA APLICACIÓN

if __name__ == '__main__':
    with app.app_context():
        db.create_all()  # Crea las tablas si no existen
    app.run(debug=True)
×
×
×
¿Quieres regenerar la respuesta?
×
Exportar chat
Elige el formato de exportación
⏳ Generazione PDF in corso…
Allegati
×
⚠️ Estás a punto de cerrar el chat y pasar al generador de imágenes. Si no has iniciado sesión, perderás nuestro chat. ¿Confirmas?
👁 Estás viendo un chat compartido en modo temporal. No se guardará.
💬
×
Prompts guardados
×
Nota privada
×
Etiqueta
×
Buscar en todos los chats
×
Tus insights
Analizando…
×
Preferencias de IA
Nombre del asistente 30
Longitud de las respuestas
Modelo de IA
Respuestas estructuradas
Incluir ejemplos
Usar emojis
Guíame, no resuelvas Pistas y preguntas en vez de la solución hecha
Más ajustes →
×
Compartir este chat
Quien abra este enlace podrá ver el chat o añadirlo a su perfil como chat propio.
⚠️ Atención: también se compartirán los archivos adjuntos del chat. Quien lo añada recibirá una copia en su propia carpeta.
Chat compartido
Alguien ha compartido un chat contigo. ¿Solo quieres verlo o añadirlo a tus chats?
⚠️ Atención: también se compartirán los archivos adjuntos del chat. Quien lo añada recibirá una copia en su propia carpeta.
×

📌 Mensajes guardados

Cargando...

×

Historial de Chat

programas-ejemplo · HISTORIAL DE CHAT

Cargando...

Preferencias de IA

×
  • 🟢 BásicoRespuestas rápidas y esenciales para estudiar
  • 🔵 MedioMayor calidad para estudio y programación
  • 🟣 AvanzadoRazonamiento complejo y análisis detallado
Explicar Pasos

Ejemplo Completo

# Ejemplo completo para probar la aplicación Flask de gestión Kanban
# Incluye creación de la base de datos, ejecución del servidor y una petición de prueba

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
from passlib.hash import pbkdf2_sha256 as sha256
from marshmallow import Schema, fields, ValidationError
from datetime import datetime, timedelta
import json

# Código original copiado exactamente

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
from passlib.hash import pbkdf2_sha256 as sha256
from marshmallow import Schema, fields, ValidationError
from datetime import datetime, timedelta

# Configuración de la aplicación y base de datos
class Config:
    SECRET_KEY = 'super-secret-key-change-me'
    JWT_SECRET_KEY = 'jwt-super-secret-key-change-me'
    SQLALCHEMY_DATABASE_URI = 'sqlite:///kanban.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False

app = Flask(__name__)
app.config.from_object(Config)

db = SQLAlchemy(app)
jwt = JWTManager(app)

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    projects = db.relationship('Project', backref='owner', lazy=True)

    @staticmethod
    def generate_hash(password):
        return sha256.hash(password)

    @staticmethod
    def verify_hash(password, hash_):
        return sha256.verify(password, hash_)

class Project(db.Model):
    __tablename__ = 'projects'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(120), nullable=False)
    description = db.Column(db.Text, nullable=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    columns = db.relationship('Column', backref='project', lazy=True, cascade='all, delete-orphan')

class Column(db.Model):
    __tablename__ = 'columns'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(120), nullable=False)
    project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False)
    tasks = db.relationship('Task', backref='column', lazy=True, cascade='all, delete-orphan')

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    description = db.Column(db.Text, nullable=True)
    status = db.Column(db.String(50), nullable=False)  # ej. 'todo', 'in-progress', 'done'
    priority = db.Column(db.Integer, default=3)  # 1=alta, 2=media, 3=baja
    due_date = db.Column(db.DateTime, nullable=True)
    assigned_to = db.Column(db.String(80), nullable=True)  # nombre de usuario asignado
    column_id = db.Column(db.Integer, db.ForeignKey('columns.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

class UserSchema(Schema):
    id = fields.Int(dump_only=True)
    username = fields.Str(required=True)
    password = fields.Str(load_only=True, required=True)

class ProjectSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True)
    description = fields.Str()
    user_id = fields.Int(dump_only=True)

class ColumnSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True)
    project_id = fields.Int(required=True)

class TaskSchema(Schema):
    id = fields.Int(dump_only=True)
    title = fields.Str(required=True)
    description = fields.Str()
    status = fields.Str(required=True)
    priority = fields.Int(validate=lambda x: 1 <= x <= 3)
    due_date = fields.DateTime(allow_none=True)
    assigned_to = fields.Str(allow_none=True)
    column_id = fields.Int(required=True)
    created_at = fields.DateTime(dump_only=True)
    updated_at = fields.DateTime(dump_only=True)

user_schema = UserSchema()
project_schema = ProjectSchema()
projects_schema = ProjectSchema(many=True)
column_schema = ColumnSchema()
columns_schema = ColumnSchema(many=True)
task_schema = TaskSchema()
tasks_schema = TaskSchema(many=True)

@app.route('/register', methods=['POST'])
def register():
    data = request.get_json()
    try:
        user_data = user_schema.load(data)
    except ValidationError as err:
        return jsonify(err.messages), 400

    if User.query.filter_by(username=user_data['username']).first():
        return jsonify({'message': 'Nombre de usuario ya existente'}), 400

    new_user = User(
        username=user_data['username'],
        password_hash=User.generate_hash(user_data['password'])
    )
    db.session.add(new_user)
    db.session.commit()

    return jsonify({'message': 'Usuario registrado con éxito'}), 201

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    if not data or not data.get('username') or not data.get('password'):
        return jsonify({'message': 'Nombre de usuario y contraseña requeridos'}), 400

    user = User.query.filter_by(username=data['username']).first()
    if not user or not User.verify_hash(data['password'], user.password_hash):
        return jsonify({'message': 'Credenciales no válidas'}), 401

    access_token = create_access_token(identity=user.id, expires_delta=timedelta(hours=1))
    return jsonify({'access_token': access_token}), 200

def get_current_user():
    user_id = get_jwt_identity()
    return User.query.get(user_id)

@app.route('/projects', methods=['POST'])
@jwt_required()
def create_project():
    user = get_current_user()
    data = request.get_json()
    try:
        project_data = project_schema.load(data)
    except ValidationError as err:
        return jsonify(err.messages), 400

    new_project = Project(
        name=project_data['name'],
        description=project_data.get('description'),
        owner=user
    )
    db.session.add(new_project)
    db.session.commit()

    return project_schema.jsonify(new_project), 201

@app.route('/projects', methods=['GET'])
@jwt_required()
def get_projects():
    user = get_current_user()
    projects = Project.query.filter_by(user_id=user.id).all()
    return projects_schema.jsonify(projects), 200

@app.route('/projects/<int:project_id>', methods=['GET'])
@jwt_required()
def get_project(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404
    return project_schema.jsonify(project), 200

@app.route('/projects/<int:project_id>', methods=['PUT'])
@jwt_required()
def update_project(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404

    data = request.get_json()
    try:
        project_data = project_schema.load(data, partial=True)
    except ValidationError as err:
        return jsonify(err.messages), 400

    if 'name' in project_data:
        project.name = project_data['name']
    if 'description' in project_data:
        project.description = project_data['description']

    db.session.commit()
    return project_schema.jsonify(project), 200

@app.route('/projects/<int:project_id>', methods=['DELETE'])
@jwt_required()
def delete_project(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404

    db.session.delete(project)
    db.session.commit()
    return jsonify({'message': 'Proyecto eliminado'}), 200

@app.route('/projects/<int:project_id>/columns', methods=['POST'])
@jwt_required()
def create_column(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404

    data = request.get_json()
    try:
        column_data = column_schema.load({**data, 'project_id': project_id})
    except ValidationError as err:
        return jsonify(err.messages), 400

    new_column = Column(name=column_data['name'], project=project)
    db.session.add(new_column)
    db.session.commit()

    return column_schema.jsonify(new_column), 201

@app.route('/projects/<int:project_id>/columns', methods=['GET'])
@jwt_required()
def get_columns(project_id):
    user = get_current_user()
    project = Project.query.filter_by(id=project_id, user_id=user.id).first()
    if not project:
        return jsonify({'message': 'Proyecto no encontrado'}), 404

    columns = Column.query.filter_by(project_id=project_id).all()
    return columns_schema.jsonify(columns), 200

@app.route('/columns/<int:column_id>', methods=['PUT'])
@jwt_required()
def update_column(column_id):
    user = get_current_user()
    column = Column.query.get(column_id)
    if not column or column.project.user_id != user.id:
        return jsonify({'message': 'Columna no encontrada o acceso denegado'}), 404

    data = request.get_json()
    try:
        column_data = column_schema.load(data, partial=True)
    except ValidationError as err:
        return jsonify(err.messages), 400

    if 'name' in column_data:
        column.name = column_data['name']

    db.session.commit()
    return column_schema.jsonify(column), 200

@app.route('/columns/<int:column_id>', methods=['DELETE'])
@jwt_required()
def delete_column(column_id):
    user = get_current_user()
    column = Column.query.get(column_id)
    if not column or column.project.user_id != user.id:
        return jsonify({'message': 'Columna no encontrada o acceso denegado'}), 404

    db.session.delete(column)
    db.session.commit()
    return jsonify({'message': 'Columna eliminada'}), 200

@app.route('/columns/<int:column_id>/tasks', methods=['POST'])
@jwt_required()
def create_task(column_id):
    user = get_current_user()
    column = Column.query.get(column_id)
    if not column or column.project.user_id != user.id:
        return jsonify({'message': 'Columna no encontrada o acceso denegado'}), 404

    data = request.get_json()
    try:
        task_data = task_schema.load({**data, 'column_id': column_id})
    except ValidationError as err:
        return jsonify(err.messages), 400

    new_task = Task(
        title=task_data['title'],
        description=task_data.get('description'),
        status=task_data['status'],
        priority=task_data.get('priority', 3),
        due_date=task_data.get('due_date'),
        assigned_to=task_data.get('assigned_to'),
        column=column
    )
    db.session.add(new_task)
    db.session.commit()

    return task_schema.jsonify(new_task), 201

@app.route('/columns/<int:column_id>/tasks', methods=['GET'])
@jwt_required()
def get_tasks(column_id):
    user = get_current_user()
    column = Column.query.get(column_id)
    if not column or column.project.user_id != user.id:
        return jsonify({'message': 'Columna no encontrada o acceso denegado'}), 404

    status = request.args.get('status')
    assigned_to = request.args.get('assigned_to')
    priority = request.args.get('priority', type=int)
    due_date_from = request.args.get('due_date_from')
    due_date_to = request.args.get('due_date_to')

    query = Task.query.filter_by(column_id=column_id)

    if status:
        query = query.filter(Task.status == status)
    if assigned_to:
        query = query.filter(Task.assigned_to == assigned_to)
    if priority:
        query = query.filter(Task.priority == priority)
    if due_date_from:
        try:
            dt_from = datetime.fromisoformat(due_date_from)
            query = query.filter(Task.due_date >= dt_from)
        except ValueError:
            return jsonify({'message': 'Formato de fecha due_date_from no válido'}), 400
    if due_date_to:
        try:
            dt_to = datetime.fromisoformat(due_date_to)
            query = query.filter(Task.due_date <= dt_to)
        except ValueError:
            return jsonify({'message': 'Formato de fecha due_date_to no válido'}), 400

    tasks = query.all()
    return tasks_schema.jsonify(tasks), 200

# Código adicional para inicializar la base de datos y ejecutar el servidor

if __name__ == '__main__':
    # Crear las tablas en la base de datos
    with app.app_context():
        db.create_all()

    # Ejecutar el servidor Flask
    app.run(debug=True)

# Comentarios:
# 1. Este script es un ejemplo mínimo y completo para probar la API REST de Kanban.
# 2. Se crea la base de datos SQLite localmente y se levantan todas las rutas.
# 3. Para probar, se puede usar curl o Postman para registrar un usuario, loguearse y crear proyectos, columnas y tareas.
# 4. El servidor corre en modo debug para facilitar pruebas y desarrollo.

¿Qué se necesita?

Hardware: No se requiere ningún componente hardware (aplicación backend del lado servidor).

Software: Python 3.8+, Flask para el framework web, Flask-JWT-Extended para autenticación JWT, Flask-SQLAlchemy para ORM, SQLite como base de datos por defecto (configurable), Marshmallow para serialización y validación.

Configuración: Archivo de configuración para claves secretas JWT y URI de base de datos (config.py).

Dependencias: flask, flask_sqlalchemy, flask_jwt_extended, marshmallow, passlib (para hashing de contraseñas).

Estructura API REST: endpoints para registro/login de usuarios, CRUD de proyectos, columnas y tareas, filtros avanzados para consultas de tareas.

Seguridad: autenticación JWT, control de acceso a recursos basado en usuario propietario.

Base de datos: modelos User, Project, Column, Task con relaciones uno-a-muchos y campos para estado, prioridad, fechas, asignados.

Temas Similares

Calculadora básica con suma y resta
Calculadora básica en Python para sumar y restar números enteros con interfaz sencilla y manejo de errores, ideal para principiantes sin experiencia previa.
Conversión de grados Celsius a Fahrenheit
Programa en Python que convierte temperaturas de grados Celsius a Fahrenheit con entrada desde teclado y salida en consola, ideal para principiantes.
Contar de 1 a 1 con un ciclo while
Ejemplo sencillo en Python que muestra cómo contar de 1 a 1 usando un ciclo while para imprimir cada número paso a paso.
Conversión de kilómetros a millas
Script en Python que convierte kilómetros a millas con manejo de errores y entrada válida. Ideal para aprender conversiones y programación básica.
Imprimir números pares del 2 al 2 con un ciclo for
Script en Python que imprime números pares del 2 al 2 usando un ciclo for, ideal para aprender ciclos y condiciones básicas en Python 3 sin dependencias extern
Concatenación de nombre y apellido en una sola cadena
Ejemplo básico en Python para concatenar nombre y apellido en una sola cadena con espacio, ideal para principiantes en programación.
Conversión de minutos a segundos
Script en Python que convierte minutos a segundos con manejo de errores para entradas no numéricas y valores negativos, mostrando resultados claros en consola.
Cálculo del precio con descuento dado el precio y el porcentaje de descuento
Script en Python para calcular el precio final con descuento aplicando un porcentaje dado, validando entradas y evitando errores comunes en el cálculo.