feat: catalogo dinamico de tipificaciones de hitos, matriz RBAC y desacoplamiento en modal de eventos
This commit is contained in:
@@ -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])
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user