feat: catalogo dinamico de tipificaciones de hitos, matriz RBAC y desacoplamiento en modal de eventos

This commit is contained in:
Carlos Tello
2026-09-03 03:48:49 -03:00
parent 0c867e0031
commit 291e537574
13 changed files with 777 additions and 23 deletions
+135
View File
@@ -4,8 +4,10 @@ from flask_babel import gettext as _
from app import db
from app.models.user import User
from app.models.role import Role, Permission, SYSTEM_MODULES, ACCESS_LEVELS
from app.models.milestone import MilestoneType, AcademicMilestone, BASE_MILESTONE_TYPES
from app.utils.decorators import admin_required, permission_required
from sqlalchemy import or_
import re
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@@ -340,3 +342,136 @@ def google_sheets_import():
recent_subjects=recent_subjects
)
# ---------------------------------------------------------
# MILESTONE TYPES & ACADEMIC CATEGORIES (ABM / CRUD)
# ---------------------------------------------------------
@admin_bp.route('/milestone-types')
@permission_required('milestone_types', 'read')
def milestone_types_list():
"""List all milestone categories / types"""
# Preload default milestone types if table is empty
if MilestoneType.query.count() == 0:
for item in BASE_MILESTONE_TYPES:
mt = MilestoneType(
name=item['name'],
code=item['code'],
color=item['color'],
description=item['description'],
sort_order=item['sort_order'],
is_active=True
)
db.session.add(mt)
db.session.commit()
types = MilestoneType.query.order_by(MilestoneType.sort_order.asc(), MilestoneType.id.asc()).all()
can_edit = current_user.has_permission('milestone_types', 'read_write')
return render_template('admin/milestone_types/list.html', types=types, can_edit=can_edit)
@admin_bp.route('/milestone-types/add', methods=['POST'])
@permission_required('milestone_types', 'read_write')
def milestone_type_add():
"""Create a new milestone type"""
name = request.form.get('name', '').strip()
code = request.form.get('code', '').strip().lower()
color = request.form.get('color', '#0d6efd').strip()
description = request.form.get('description', '').strip()
sort_order = request.form.get('sort_order', 0, type=int)
is_active = True if request.form.get('is_active') in ['1', 'true', 'True', 'on'] else False
if not name:
flash(_('El nombre de la tipificación es obligatorio.'), 'error')
return redirect(url_for('admin.milestone_types_list'))
# Auto-generate code slug if not provided
if not code:
code = re.sub(r'[^a-zA-Z0-9_]+', '_', name.lower()).strip('_')
# Check code uniqueness
existing = MilestoneType.query.filter_by(code=code).first()
if existing:
flash(_('Ya existe una tipificación con el código "%(code)s".', code=code), 'error')
return redirect(url_for('admin.milestone_types_list'))
new_type = MilestoneType(
name=name,
code=code,
color=color or '#0d6efd',
description=description,
sort_order=sort_order,
is_active=is_active
)
db.session.add(new_type)
db.session.commit()
flash(_('Tipificación "%(name)s" creada exitosamente.', name=new_type.name), 'success')
return redirect(url_for('admin.milestone_types_list'))
@admin_bp.route('/milestone-types/edit/<int:id>', methods=['POST'])
@permission_required('milestone_types', 'read_write')
def milestone_type_edit(id):
"""Edit an existing milestone type"""
item = MilestoneType.query.get_or_404(id)
name = request.form.get('name', '').strip()
code = request.form.get('code', '').strip().lower()
color = request.form.get('color', '#0d6efd').strip()
description = request.form.get('description', '').strip()
sort_order = request.form.get('sort_order', item.sort_order, type=int)
is_active = True if request.form.get('is_active') in ['1', 'true', 'True', 'on'] else False
if not name:
flash(_('El nombre de la tipificación es obligatorio.'), 'error')
return redirect(url_for('admin.milestone_types_list'))
if not code:
code = re.sub(r'[^a-zA-Z0-9_]+', '_', name.lower()).strip('_')
# Check code uniqueness against other records
existing = MilestoneType.query.filter(MilestoneType.code == code, MilestoneType.id != id).first()
if existing:
flash(_('Ya existe otra tipificación con el código "%(code)s".', code=code), 'error')
return redirect(url_for('admin.milestone_types_list'))
item.name = name
item.code = code
item.color = color or item.color
item.description = description
item.sort_order = sort_order
item.is_active = is_active
db.session.commit()
flash(_('Tipificación "%(name)s" actualizada correctamente.', name=item.name), 'success')
return redirect(url_for('admin.milestone_types_list'))
@admin_bp.route('/milestone-types/toggle/<int:id>', methods=['POST'])
@permission_required('milestone_types', 'read_write')
def milestone_type_toggle(id):
"""Toggle active status of a milestone type"""
item = MilestoneType.query.get_or_404(id)
item.is_active = not item.is_active
db.session.commit()
if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'success': True,
'id': item.id,
'is_active': item.is_active,
'message': _('Tipificación "%(name)s" %(status)s.', name=item.name, status=_('activada') if item.is_active else _('desactivada'))
})
status_str = _('activada') if item.is_active else _('desactivada')
flash(_('Tipificación "%(name)s" ha sido %(status)s.', name=item.name, status=status_str), 'success')
return redirect(url_for('admin.milestone_types_list'))
@admin_bp.route('/api/milestone-types')
@login_required
def api_milestone_types():
"""API endpoint to get active milestone types for forms and dropdowns"""
types = MilestoneType.query.filter_by(is_active=True).order_by(MilestoneType.sort_order.asc(), MilestoneType.id.asc()).all()
return jsonify([t.to_dict() for t in types])
+22 -1
View File
@@ -134,4 +134,25 @@ def set_language(language=None):
current_user.preferred_language = language
db.session.commit()
return redirect(request.referrer or url_for('main.dashboard'))␍
return redirect(request.referrer or url_for('main.dashboard'))
@main_bp.route('/api/milestone-types')
def api_milestone_types():
"""Retorna las tipificaciones de hitos activas en JSON para consumo de frontend/mobile"""
from app.models.milestone import MilestoneType, BASE_MILESTONE_TYPES
if MilestoneType.query.count() == 0:
for item in BASE_MILESTONE_TYPES:
mt = MilestoneType(
name=item['name'],
code=item['code'],
color=item['color'],
description=item['description'],
sort_order=item['sort_order'],
is_active=True
)
db.session.add(mt)
db.session.commit()
types = MilestoneType.query.filter_by(is_active=True).order_by(MilestoneType.sort_order.asc(), MilestoneType.id.asc()).all()
return jsonify([t.to_dict() for t in types])␍