feat: abm comisiones, hitos, links virtuales, eliminacion usuarios y roles bedelia y alumno
This commit is contained in:
@@ -284,6 +284,30 @@
|
||||
* Se trasladó el modal de confirmación de eliminación `#deleteRoleModal` fuera de la celda `<td>` y de la tabla `<table>`, situándolo a nivel raíz de la plantilla.
|
||||
* Se corrigió el error visual donde el cuadro emergente temblaba, saltaba o se desplazaba por el recálculo de padding/scrollbar de Bootstrap en la celda de la tabla.
|
||||
|
||||
---
|
||||
|
||||
## 18. ABM de Comisiones con Hitos y Aula Virtual, Eliminación de Usuarios y Renombrado de Roles (Bedelía y Alumno)
|
||||
* **Renombrado de Roles Institucionales ([init_db.py](file:///c:/Workspace/admin-edu-space/init_db.py), base de datos PostgreSQL):**
|
||||
* `Operador` pasó a denominarse **`Bedelía`** con foco en la gestión operativa de aulas, sedes, reservas, comisiones, importación y cartelera.
|
||||
* `Consulta` pasó a denominarse **`Alumno`** con acceso de solo lectura al cronograma general, asignaturas y aulas.
|
||||
* Se ejecutó la actualización en caliente de los registros existentes en PostgreSQL y se actualizaron los sembrados en `init_db.py`.
|
||||
* **Corrección Visual de Tipificaciones de Hitos ([app/templates/admin/milestone_types/list.html](file:///c:/Workspace/admin-edu-space/app/templates/admin/milestone_types/list.html)):**
|
||||
* Se ajustó el contenedor a `pt-5 mt-4` evitando que el encabezado quede solapado bajo la barra de navegación fija.
|
||||
* Se limpió la duplicación del nombre en la tabla (se eliminó la repetición del texto dentro de la celda).
|
||||
* Se agregó la paleta rápida de colores institucionales UniCABA al modal de edición.
|
||||
* Se agregó el botón y modal global de confirmación de eliminación `#deleteMilestoneTypeModal` sin saltos visuales.
|
||||
* **Eliminación de Usuarios ([app/routes/admin.py](file:///c:/Workspace/admin-edu-space/app/routes/admin.py), [app/templates/admin/users/list.html](file:///c:/Workspace/admin-edu-space/app/templates/admin/users/list.html)):**
|
||||
* Se implementó el endpoint `POST /admin/users/delete/<id>` protegido contra la eliminación del usuario en sesión activa y del administrador único.
|
||||
* Desvinculación automática de comisiones como docente para prevenir violaciones de integridad referencial.
|
||||
* Botón rojo de eliminación con ícono de papelera y modal global `#deleteUserModal` fuera de la tabla para evitar desplazamientos.
|
||||
* **ABM Integral de Comisiones con Aulas Virtuales e Hitos ([app/models/subject.py](file:///c:/Workspace/admin-edu-space/app/models/subject.py), [app/models/milestone.py](file:///c:/Workspace/admin-edu-space/app/models/milestone.py), [app/routes/schedule.py](file:///c:/Workspace/admin-edu-space/app/routes/schedule.py), [app/templates/schedule/commissions.html](file:///c:/Workspace/admin-edu-space/app/templates/schedule/commissions.html), [app/templates/base.html](file:///c:/Workspace/admin-edu-space/app/templates/base.html)):**
|
||||
* **Columna y Enlace de Aula Virtual:** Se añadió el campo `virtual_link` (VARCHAR 500) a `commissions`. Permite registrar enlaces directos a Zoom, Microsoft Teams, Google Meet o Campus Virtual.
|
||||
* **Hitos de Evaluación vinculados a Comisiones:** Se añadió `commission_id` a `academic_milestones`. Permite asociar parciales, TPs y recuperatorios directamente a la comisión de cursada.
|
||||
* **Permisos para Profesores, Bedelía y Admin:** Los roles `Docente`, `Bedelía` y `Admin` pueden configurar el aula virtual y registrar hitos académicos en sus comisiones.
|
||||
* **Navegación:** Acceso directo a *Comisiones y Cursadas* disponible en la barra superior (menús *Schedule* y *Management*).
|
||||
* **Modales interactivos:** Creación de comisión (`#modalAddCommission`), edición (`#modalEditCommission`), configuración de aula virtual (`#modalVirtualLink`), programación de hitos (`#modalAddMilestone`) y eliminación segura (`#deleteCommissionModal`).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class AcademicMilestone(db.Model):
|
||||
title = db.Column(db.String(150), nullable=True)
|
||||
materia_codigo = db.Column(db.String(50), nullable=False, index=True) # Código de materia (ej: INGA0003)
|
||||
subject_id = db.Column(db.Integer, db.ForeignKey('subjects.id'), nullable=True)
|
||||
commission_id = db.Column(db.Integer, db.ForeignKey('commissions.id', ondelete='SET NULL'), nullable=True)
|
||||
milestone_type_id = db.Column(db.Integer, db.ForeignKey('milestone_types.id'), nullable=False)
|
||||
date = db.Column(db.DateTime, nullable=False) # Fecha y hora del hito
|
||||
classroom = db.Column(db.String(100), nullable=True) # Aula o enlace virtual asignado
|
||||
@@ -56,8 +57,9 @@ class AcademicMilestone(db.Model):
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relación con Asignatura
|
||||
# Relación con Asignatura y Comisión
|
||||
subject = db.relationship('Subject', backref=db.backref('milestones', lazy='dynamic'))
|
||||
commission = db.relationship('Commission', backref=db.backref('academic_milestones', lazy='dynamic', cascade='all, delete-orphan'))
|
||||
|
||||
@property
|
||||
def tipo(self):
|
||||
@@ -72,6 +74,8 @@ class AcademicMilestone(db.Model):
|
||||
'id': self.id,
|
||||
'title': self.title or self.tipo,
|
||||
'materiaCodigo': self.materia_codigo,
|
||||
'subject_id': self.subject_id,
|
||||
'commission_id': self.commission_id,
|
||||
'tipo': self.tipo,
|
||||
'milestone_type_id': self.milestone_type_id,
|
||||
'tipo_info': self.type_obj.to_dict() if self.type_obj else None,
|
||||
|
||||
@@ -50,6 +50,7 @@ class Commission(db.Model):
|
||||
current_students = db.Column(db.Integer, default=0)
|
||||
schedule = db.Column(db.String(255))
|
||||
shift = db.Column(db.String(30), nullable=True) # Mañana, Tarde, Vespertino, Noche
|
||||
virtual_link = db.Column(db.String(500), nullable=True) # Link Zoom, Meet, Teams, Moodle
|
||||
active = db.Column(db.Boolean, default=True) # Changed from is_active to active
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
@@ -87,6 +88,8 @@ class Commission(db.Model):
|
||||
'max_students': self.max_students,
|
||||
'current_students': self.current_students,
|
||||
'schedule': self.schedule,
|
||||
'shift': self.shift,
|
||||
'virtual_link': self.virtual_link,
|
||||
'active': self.active,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
|
||||
@@ -179,6 +179,36 @@ def user_toggle(id):
|
||||
return redirect(url_for('admin.users_list'))
|
||||
|
||||
|
||||
@admin_bp.route('/users/delete/<int:id>', methods=['POST'])
|
||||
@admin_required
|
||||
def user_delete(id):
|
||||
"""Delete a user account"""
|
||||
if id == current_user.id:
|
||||
flash(_('No puedes eliminar tu propia cuenta mientras estás en sesión activa.'), 'error')
|
||||
return redirect(url_for('admin.users_list'))
|
||||
|
||||
user = User.query.get_or_404(id)
|
||||
|
||||
# Protect sole system administrator
|
||||
if user.is_admin():
|
||||
admin_count = User.query.filter(User.role.ilike('%ADMIN%'), User.is_active == True).count()
|
||||
if admin_count <= 1:
|
||||
flash(_('No es posible eliminar al único administrador activo del sistema.'), 'error')
|
||||
return redirect(url_for('admin.users_list'))
|
||||
|
||||
# Unassign user from commissions as teacher
|
||||
from app.models.subject import Commission
|
||||
Commission.query.filter_by(teacher_id=id).update({'teacher_id': None})
|
||||
|
||||
name = user.name
|
||||
email = user.email
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
flash(_('El usuario "%(name)s" (%(email)s) ha sido eliminado del sistema.', name=name, email=email), 'success')
|
||||
return redirect(url_for('admin.users_list'))
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# ROLES & PERMISSIONS MANAGEMENT (FortiGate Matrix)
|
||||
# ---------------------------------------------------------
|
||||
@@ -467,6 +497,27 @@ def milestone_type_toggle(id):
|
||||
return redirect(url_for('admin.milestone_types_list'))
|
||||
|
||||
|
||||
@admin_bp.route('/milestone-types/delete/<int:id>', methods=['POST'])
|
||||
@permission_required('milestone_types', 'read_write')
|
||||
def milestone_type_delete(id):
|
||||
"""Delete a milestone type if unused, or deactivate with notice"""
|
||||
item = MilestoneType.query.get_or_404(id)
|
||||
from app.models.milestone import AcademicMilestone
|
||||
milestone_count = AcademicMilestone.query.filter_by(milestone_type_id=id).count()
|
||||
|
||||
if milestone_count > 0:
|
||||
item.is_active = False
|
||||
db.session.commit()
|
||||
flash(_('La tipificación "%(name)s" tiene %(count)d hitos registrados. Se ha desactivado para conservar el historial académico.', name=item.name, count=milestone_count), 'warning')
|
||||
else:
|
||||
name = item.name
|
||||
db.session.delete(item)
|
||||
db.session.commit()
|
||||
flash(_('Tipificación "%(name)s" eliminada permanentemente.', name=name), 'success')
|
||||
|
||||
return redirect(url_for('admin.milestone_types_list'))
|
||||
|
||||
|
||||
@admin_bp.route('/api/milestone-types')
|
||||
@login_required
|
||||
def api_milestone_types():
|
||||
|
||||
+229
-6
@@ -157,19 +157,242 @@ def calendar_view():
|
||||
available_floors=available_floors
|
||||
)
|
||||
|
||||
@schedule_bp.route('/commissions')
|
||||
def commissions_view():
|
||||
subjects = Subject.query.all()
|
||||
def user_can_manage_commission_milestones(user, commission=None):
|
||||
"""Verifica si el usuario (Docente, Admin, Bedelía) puede gestionar hitos y links virtuales"""
|
||||
if user.is_admin():
|
||||
return True
|
||||
role_name = (user.role_obj.name if user.role_obj else user.role or '').lower()
|
||||
if role_name in ['admin', 'bedelia', 'bedelía', 'operador']:
|
||||
return True
|
||||
if role_name in ['docente', 'profesor']:
|
||||
return True
|
||||
return user.has_permission('schedule', 'read_write')
|
||||
|
||||
# Calculamos el total de capacidad sumando todas las comisiones de todas las materias
|
||||
|
||||
@schedule_bp.route('/commissions')
|
||||
@login_required
|
||||
def commissions_view():
|
||||
"""Vista y ABM de Comisiones con soporte de enlace virtual e hitos académicos"""
|
||||
from app.models.subject import Commission, Subject
|
||||
from app.models.classroom import Classroom
|
||||
from app.models.user import User
|
||||
from app.models.milestone import MilestoneType
|
||||
|
||||
subjects = Subject.query.order_by(Subject.code.asc()).all()
|
||||
classrooms = Classroom.query.filter_by(is_active=True).order_by(Classroom.code.asc()).all()
|
||||
teachers = User.query.filter_by(is_active=True).order_by(User.name.asc()).all()
|
||||
milestone_types = MilestoneType.query.filter_by(is_active=True).order_by(MilestoneType.sort_order.asc()).all()
|
||||
|
||||
# Totales para indicadores superiores
|
||||
total_capacity = sum(c.max_students for s in subjects for c in s.commissions)
|
||||
# Calculamos el total de comisiones
|
||||
total_commissions = sum(len(s.commissions) for s in subjects)
|
||||
|
||||
can_manage_milestones = user_can_manage_commission_milestones(current_user)
|
||||
role_name = (current_user.role_obj.name if current_user.role_obj else current_user.role or '').lower()
|
||||
can_edit_commissions = current_user.is_admin() or role_name in ['admin', 'bedelia', 'bedelía', 'operador'] or current_user.has_permission('schedule', 'read_write')
|
||||
|
||||
return render_template('schedule/commissions.html',
|
||||
subjects=subjects,
|
||||
classrooms=classrooms,
|
||||
teachers=teachers,
|
||||
milestone_types=milestone_types,
|
||||
total_capacity=total_capacity,
|
||||
total_commissions=total_commissions)
|
||||
total_commissions=total_commissions,
|
||||
can_manage_milestones=can_manage_milestones,
|
||||
can_edit_commissions=can_edit_commissions)
|
||||
|
||||
|
||||
@schedule_bp.route('/commissions/add', methods=['POST'])
|
||||
@login_required
|
||||
def commission_add():
|
||||
"""Crear una nueva comisión de cursada"""
|
||||
role_name = (current_user.role_obj.name if current_user.role_obj else current_user.role or '').lower()
|
||||
if not (current_user.is_admin() or role_name in ['admin', 'bedelia', 'bedelía', 'operador'] or current_user.has_permission('schedule', 'read_write')):
|
||||
flash(_('No tienes permisos para crear comisiones.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
from app.models.subject import Commission
|
||||
subject_id = request.form.get('subject_id', type=int)
|
||||
code = request.form.get('code', '').strip()
|
||||
semester = request.form.get('semester', '1C').strip()
|
||||
year = request.form.get('year', datetime.now().year, type=int)
|
||||
teacher_id = request.form.get('teacher_id', type=int) or None
|
||||
max_students = request.form.get('max_students', 40, type=int)
|
||||
schedule = request.form.get('schedule', '').strip()
|
||||
shift = request.form.get('shift', 'Tarde').strip()
|
||||
virtual_link = request.form.get('virtual_link', '').strip()
|
||||
|
||||
if not subject_id or not code:
|
||||
flash(_('La asignatura y el código de comisión son obligatorios.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
# Verificar existencia duplicada
|
||||
existing = Commission.query.filter_by(subject_id=subject_id, code=code, semester=semester, year=year).first()
|
||||
if existing:
|
||||
flash(_('Ya existe una comisión %(code)s para el ciclo %(sem)s %(yr)d.', code=code, sem=semester, yr=year), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
new_comm = Commission(
|
||||
subject_id=subject_id,
|
||||
code=code,
|
||||
semester=semester,
|
||||
year=year,
|
||||
teacher_id=teacher_id,
|
||||
max_students=max_students,
|
||||
schedule=schedule,
|
||||
shift=shift,
|
||||
virtual_link=virtual_link or None,
|
||||
active=True
|
||||
)
|
||||
db.session.add(new_comm)
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Comisión %(code)s creada exitosamente.', code=new_comm.get_full_code()), 'success')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
|
||||
@schedule_bp.route('/commissions/<int:id>/edit', methods=['POST'])
|
||||
@login_required
|
||||
def commission_edit(id):
|
||||
"""Editar datos de una comisión"""
|
||||
role_name = (current_user.role_obj.name if current_user.role_obj else current_user.role or '').lower()
|
||||
if not (current_user.is_admin() or role_name in ['admin', 'bedelia', 'bedelía', 'operador'] or current_user.has_permission('schedule', 'read_write')):
|
||||
flash(_('No tienes permisos para modificar comisiones.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
from app.models.subject import Commission
|
||||
commission = Commission.query.get_or_404(id)
|
||||
|
||||
commission.code = request.form.get('code', commission.code).strip()
|
||||
commission.semester = request.form.get('semester', commission.semester).strip()
|
||||
commission.year = request.form.get('year', commission.year, type=int)
|
||||
commission.teacher_id = request.form.get('teacher_id', type=int) or None
|
||||
commission.max_students = request.form.get('max_students', commission.max_students, type=int)
|
||||
commission.schedule = request.form.get('schedule', commission.schedule).strip()
|
||||
commission.shift = request.form.get('shift', commission.shift).strip()
|
||||
commission.virtual_link = request.form.get('virtual_link', '').strip() or None
|
||||
commission.active = True if request.form.get('active') in ['1', 'true', 'on'] else False
|
||||
|
||||
db.session.commit()
|
||||
flash(_('Comisión %(code)s actualizada con éxito.', code=commission.get_full_code()), 'success')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
|
||||
@schedule_bp.route('/commissions/<int:id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def commission_delete(id):
|
||||
"""Eliminar una comisión"""
|
||||
role_name = (current_user.role_obj.name if current_user.role_obj else current_user.role or '').lower()
|
||||
if not (current_user.is_admin() or role_name in ['admin', 'bedelia', 'bedelía', 'operador'] or current_user.has_permission('schedule', 'read_write')):
|
||||
flash(_('No tienes permisos para eliminar comisiones.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
from app.models.subject import Commission
|
||||
commission = Commission.query.get_or_404(id)
|
||||
full_code = commission.get_full_code()
|
||||
|
||||
db.session.delete(commission)
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Comisión %(code)s eliminada permanentemente.', code=full_code), 'success')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
|
||||
@schedule_bp.route('/commissions/<int:id>/virtual-link', methods=['POST'])
|
||||
@login_required
|
||||
def commission_virtual_link(id):
|
||||
"""Actualizar enlace del aula virtual (Zoom, Meet, Teams, Moodle) - Profesores, Admin y Bedelía"""
|
||||
from app.models.subject import Commission
|
||||
commission = Commission.query.get_or_404(id)
|
||||
|
||||
if not user_can_manage_commission_milestones(current_user, commission):
|
||||
flash(_('No tienes permisos para actualizar el aula virtual de esta comisión.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
link = request.form.get('virtual_link', '').strip()
|
||||
commission.virtual_link = link or None
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Enlace del aula virtual actualizado para la comisión %(code)s.', code=commission.get_full_code()), 'success')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
|
||||
@schedule_bp.route('/commissions/<int:id>/milestones/add', methods=['POST'])
|
||||
@login_required
|
||||
def commission_milestone_add(id):
|
||||
"""Agregar un hito académico (Parcial, TP, Recuperatorio) a una comisión - Profesores, Admin y Bedelía"""
|
||||
from app.models.subject import Commission
|
||||
from app.models.milestone import AcademicMilestone, MilestoneType
|
||||
commission = Commission.query.get_or_404(id)
|
||||
|
||||
if not user_can_manage_commission_milestones(current_user, commission):
|
||||
flash(_('No tienes permisos para agregar hitos a esta comisión.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
milestone_type_id = request.form.get('milestone_type_id', type=int)
|
||||
title = request.form.get('title', '').strip()
|
||||
date_str = request.form.get('date', '').strip()
|
||||
time_str = request.form.get('time', '18:00').strip()
|
||||
classroom = request.form.get('classroom', '').strip()
|
||||
link = request.form.get('link', '').strip() or commission.virtual_link or ''
|
||||
notes = request.form.get('notes', '').strip()
|
||||
|
||||
if not milestone_type_id or not date_str:
|
||||
flash(_('Debes especificar el tipo de hito y la fecha.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
m_type = MilestoneType.query.get(milestone_type_id)
|
||||
if not m_type:
|
||||
flash(_('Tipo de hito inválido.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
try:
|
||||
dt = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
|
||||
except ValueError:
|
||||
try:
|
||||
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
flash(_('Formato de fecha inválido.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
milestone = AcademicMilestone(
|
||||
title=title or m_type.name,
|
||||
materia_codigo=commission.subject.code if commission.subject else 'ASIG',
|
||||
subject_id=commission.subject_id,
|
||||
commission_id=commission.id,
|
||||
milestone_type_id=m_type.id,
|
||||
date=dt,
|
||||
classroom=classroom or 'Campus Virtual',
|
||||
link=link or None,
|
||||
notes=notes or None,
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(milestone)
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Hito "%(title)s" registrado correctamente en %(comm)s.', title=milestone.title, comm=commission.get_full_code()), 'success')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
|
||||
@schedule_bp.route('/commissions/<int:commission_id>/milestones/<int:milestone_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def commission_milestone_delete(commission_id, milestone_id):
|
||||
"""Eliminar un hito académico de una comisión"""
|
||||
from app.models.subject import Commission
|
||||
from app.models.milestone import AcademicMilestone
|
||||
commission = Commission.query.get_or_404(commission_id)
|
||||
|
||||
if not user_can_manage_commission_milestones(current_user, commission):
|
||||
flash(_('No tienes permisos para eliminar hitos de esta comisión.'), 'error')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
milestone = AcademicMilestone.query.filter_by(id=milestone_id, commission_id=commission_id).first_or_404()
|
||||
title = milestone.title
|
||||
db.session.delete(milestone)
|
||||
db.session.commit()
|
||||
|
||||
flash(_('Hito "%(title)s" eliminado de la comisión.', title=title), 'success')
|
||||
return redirect(url_for('schedule.commissions_view'))
|
||||
|
||||
@schedule_bp.route('/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}{% trans %}Tipificaciones de Hitos y Categorías{% endtrans %} - Edu-Space{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<div>
|
||||
@@ -16,7 +16,7 @@
|
||||
<h1 class="h3 fw-bold mb-1">
|
||||
<i class="bi bi-tags text-warning me-2"></i>{% trans %}Tipificaciones de Hitos Académicos{% endtrans %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
<p class="text-muted mb-0 small">
|
||||
{% trans %}Catálogo dinámico y configurable de tipos de evaluaciones, parciales, TPs y eventos para el calendario institucional.{% endtrans %}
|
||||
</p>
|
||||
</div>
|
||||
@@ -43,9 +43,9 @@
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-body-tertiary">
|
||||
<tr>
|
||||
<th class="ps-4" style="width: 70px;">{% trans %}Color{% endtrans %}</th>
|
||||
<th>{% trans %}Nombre Visible{% endtrans %}</th>
|
||||
<th class="ps-4">{% trans %}Tipificación e Insignia{% endtrans %}</th>
|
||||
<th>{% trans %}Código / Slug{% endtrans %}</th>
|
||||
<th>{% trans %}Color Hex{% endtrans %}</th>
|
||||
<th>{% trans %}Descripción{% endtrans %}</th>
|
||||
<th class="text-center">{% trans %}Orden{% endtrans %}</th>
|
||||
<th class="text-center">{% trans %}Estado{% endtrans %}</th>
|
||||
@@ -56,19 +56,24 @@
|
||||
{% for item in types %}
|
||||
<tr id="row-{{ item.id }}">
|
||||
<td class="ps-4">
|
||||
<span class="d-inline-block rounded-circle shadow-sm border border-light"
|
||||
style="width: 26px; height: 26px; background-color: {{ item.color or '#0d6efd' }}; vertical-align: middle;"
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="rounded-circle shadow-xs me-3 flex-shrink-0 border"
|
||||
style="width: 22px; height: 22px; background-color: {{ item.color or '#0d6efd' }};"
|
||||
title="{{ item.color }}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold fs-6">{{ item.name }}</div>
|
||||
<span class="badge rounded-pill text-white shadow-xs" style="background-color: {{ item.color or '#0d6efd' }}; font-size: 0.72rem;">
|
||||
<div>
|
||||
<span class="fw-bold text-dark-emphasis">{{ item.name }}</span>
|
||||
<span class="badge rounded-pill text-white ms-2 shadow-xs" style="background-color: {{ item.color or '#0d6efd' }}; font-size: 0.70rem;">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-primary fw-semibold">{{ item.code }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-monospace small text-muted">{{ item.color or '#0d6efd' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted small">{{ item.description or '—' }}</span>
|
||||
</td>
|
||||
@@ -94,20 +99,30 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-sm btn-outline-primary py-1 px-2 btn-edit"
|
||||
<button type="button" class="btn btn-outline-primary btn-edit"
|
||||
data-id="{{ item.id }}"
|
||||
data-name="{{ item.name }}"
|
||||
data-code="{{ item.code }}"
|
||||
data-color="{{ item.color or '#0d6efd' }}"
|
||||
data-description="{{ item.description or '' }}"
|
||||
data-sort-order="{{ item.sort_order }}"
|
||||
data-is-active="{{ 'true' if item.is_active else 'false' }}">
|
||||
<i class="bi bi-pencil-square me-1"></i>{% trans %}Editar{% endtrans %}
|
||||
data-is-active="{{ 'true' if item.is_active else 'false' }}"
|
||||
title="{% trans %}Editar Tipificación{% endtrans %}">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-delete-type"
|
||||
data-id="{{ item.id }}"
|
||||
data-name="{{ item.name }}"
|
||||
data-action-url="{{ url_for('admin.milestone_type_delete', id=item.id) }}"
|
||||
title="{% trans %}Eliminar Tipificación{% endtrans %}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-muted small"><i class="bi bi-lock me-1"></i>{% trans %}Solo lectura{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
@@ -151,41 +166,39 @@
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="add_color" class="form-label fw-semibold">{% trans %}Color Distintivo{% endtrans %}</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color w-100" id="add_color" name="color" value="#0d6efd" title="Elegir color">
|
||||
</div>
|
||||
<input type="color" class="form-control form-control-color w-100" id="add_color" name="color" value="#B43E8E" title="Elegir color">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paleta rápida de colores UniCABA -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted d-block mb-1">{% trans %}Colores sugeridos{% endtrans %}:</label>
|
||||
<div class="d-flex gap-2 flex-wrap" id="presetColorsAdd">
|
||||
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #0d6efd;" data-color="#0d6efd"></button>
|
||||
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #198754;" data-color="#198754"></button>
|
||||
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #fd7e14;" data-color="#fd7e14"></button>
|
||||
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #ffc107;" data-color="#ffc107"></button>
|
||||
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #dc3545;" data-color="#dc3545"></button>
|
||||
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #6f42c1;" data-color="#6f42c1"></button>
|
||||
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #0dcaf0;" data-color="#0dcaf0"></button>
|
||||
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #6c757d;" data-color="#6c757d"></button>
|
||||
<label class="form-label small text-muted mb-1">{% trans %}Paleta Institucional Rápida{% endtrans %}:</label>
|
||||
<div class="d-flex flex-wrap gap-2" id="presetColorsAdd">
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #B43E8E;" data-color="#B43E8E" title="Magenta UniCABA"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #0d6efd;" data-color="#0d6efd" title="Azul Institucional"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #198754;" data-color="#198754" title="Verde TP"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #fd7e14;" data-color="#fd7e14" title="Naranja Recuperatorio"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #dc3545;" data-color="#dc3545" title="Rojo Final"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #6f42c1;" data-color="#6f42c1" title="Púrpura Especial"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #20c997;" data-color="#20c997" title="Turquesa Cursada"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #6c757d;" data-color="#6c757d" title="Gris Neutro"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="add_description" class="form-label fw-semibold">{% trans %}Descripción{% endtrans %}</label>
|
||||
<textarea class="form-control" id="add_description" name="description" rows="2" placeholder="Opcional: detalles sobre el tipo de evaluación..."></textarea>
|
||||
<label for="add_description" class="form-label fw-semibold">{% trans %}Descripción / Notas{% endtrans %}</label>
|
||||
<textarea class="form-control" id="add_description" name="description" rows="2" placeholder="Detalle o alcance de este tipo de evaluación..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="add_sort_order" class="form-label fw-semibold">{% trans %}Orden de Aparición{% endtrans %}</label>
|
||||
<label for="add_sort_order" class="form-label fw-semibold">{% trans %}Orden de Visualización{% endtrans %}</label>
|
||||
<input type="number" class="form-control" id="add_sort_order" name="sort_order" value="10" min="0">
|
||||
</div>
|
||||
<div class="col-6 d-flex align-items-center pt-3">
|
||||
<div class="form-check form-switch pt-2">
|
||||
<input class="form-check-input" type="checkbox" id="add_is_active" name="is_active" value="1" checked>
|
||||
<label class="form-check-label fw-semibold" for="add_is_active">{% trans %}Activo{% endtrans %}</label>
|
||||
<label class="form-check-label fw-semibold" for="add_is_active">{% trans %}Activo de inmediato{% endtrans %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +206,7 @@
|
||||
<div class="modal-footer bg-body-tertiary">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans %}Cancelar{% endtrans %}</button>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-check-circle me-1"></i>{% trans %}Crear Tipificación{% endtrans %}
|
||||
<i class="bi bi-save me-1"></i>{% trans %}Crear Tipificación{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -229,6 +242,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paleta rápida de colores en Edición -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted mb-1">{% trans %}Paleta Institucional Rápida{% endtrans %}:</label>
|
||||
<div class="d-flex flex-wrap gap-2" id="presetColorsEdit">
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #B43E8E;" data-color="#B43E8E" title="Magenta UniCABA"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #0d6efd;" data-color="#0d6efd" title="Azul Institucional"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #198754;" data-color="#198754" title="Verde TP"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #fd7e14;" data-color="#fd7e14" title="Naranja Recuperatorio"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #dc3545;" data-color="#dc3545" title="Rojo Final"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #6f42c1;" data-color="#6f42c1" title="Púrpura Especial"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #20c997;" data-color="#20c997" title="Turquesa Cursada"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #6c757d;" data-color="#6c757d" title="Gris Neutro"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="edit_description" class="form-label fw-semibold">{% trans %}Descripción{% endtrans %}</label>
|
||||
<textarea class="form-control" id="edit_description" name="description" rows="2"></textarea>
|
||||
@@ -257,6 +285,35 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Global de Eliminación de Tipificación (fuera de la tabla) -->
|
||||
<div class="modal fade" id="deleteMilestoneTypeModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 14px;">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="badge bg-danger-subtle text-danger p-2 rounded-circle me-2">
|
||||
<i class="bi bi-exclamation-triangle-fill fs-5"></i>
|
||||
</div>
|
||||
<h5 class="modal-title fw-bold text-danger mb-0">{% trans %}Confirmar Eliminación{% endtrans %}</h5>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-start py-3">
|
||||
<p class="mb-2">{% trans %}¿Está seguro de que desea eliminar la tipificación{% endtrans %} <strong id="deleteMilestoneTypeName" class="text-primary"></strong>?</p>
|
||||
<p class="text-muted small mb-0"><i class="bi bi-info-circle me-1"></i>{% trans %}Si existen hitos vinculados a este tipo, el sistema lo desactivará automáticamente para preservar el historial.{% endtrans %}</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans %}Cancelar{% endtrans %}</button>
|
||||
<form id="deleteMilestoneTypeForm" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger px-3">
|
||||
<i class="bi bi-trash me-1"></i>{% trans %}Eliminar Tipificación{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -281,7 +338,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Color preset clicks
|
||||
// Color preset clicks for Add Modal
|
||||
document.querySelectorAll('#presetColorsAdd button').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var color = this.getAttribute('data-color');
|
||||
@@ -290,6 +347,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Color preset clicks for Edit Modal
|
||||
document.querySelectorAll('#presetColorsEdit button').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var color = this.getAttribute('data-color');
|
||||
var input = document.getElementById('edit_color');
|
||||
if (input) input.value = color;
|
||||
});
|
||||
});
|
||||
|
||||
// Edit modal bindings
|
||||
var modalEditEl = document.getElementById('modalEditType');
|
||||
var modalEdit = modalEditEl ? new bootstrap.Modal(modalEditEl) : null;
|
||||
@@ -317,6 +383,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete modal bindings
|
||||
var deleteModalEl = document.getElementById('deleteMilestoneTypeModal');
|
||||
var deleteModal = deleteModalEl ? new bootstrap.Modal(deleteModalEl) : null;
|
||||
var deleteNameEl = document.getElementById('deleteMilestoneTypeName');
|
||||
var deleteFormEl = document.getElementById('deleteMilestoneTypeForm');
|
||||
|
||||
document.querySelectorAll('.btn-delete-type').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var name = this.getAttribute('data-name');
|
||||
var actionUrl = this.getAttribute('data-action-url');
|
||||
if (deleteNameEl) deleteNameEl.textContent = name;
|
||||
if (deleteFormEl) deleteFormEl.action = actionUrl;
|
||||
if (deleteModal) deleteModal.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -136,6 +136,14 @@
|
||||
<i class="bi {% if u.is_active %}bi-person-x{% else %}bi-person-check{% endif %}"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-outline-danger btn-delete-user"
|
||||
data-user-id="{{ u.id }}"
|
||||
data-user-name="{{ u.name }}"
|
||||
data-user-email="{{ u.email }}"
|
||||
data-action-url="{{ url_for('admin.user_delete', id=u.id) }}"
|
||||
title="{% trans %}Eliminar Usuario{% endtrans %}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@@ -179,4 +187,61 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Global de Eliminación de Usuario (fuera de la tabla) -->
|
||||
<div class="modal fade" id="deleteUserModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 14px;">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="badge bg-danger-subtle text-danger p-2 rounded-circle me-2">
|
||||
<i class="bi bi-person-x-fill fs-5"></i>
|
||||
</div>
|
||||
<h5 class="modal-title fw-bold text-danger mb-0">{% trans %}Confirmar Eliminación de Usuario{% endtrans %}</h5>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-start py-3">
|
||||
<p class="mb-2">{% trans %}¿Está seguro de que desea eliminar permanentemente al siguiente usuario?{% endtrans %}</p>
|
||||
<div class="p-2 rounded bg-body-tertiary mb-2">
|
||||
<div class="fw-bold text-primary" id="deleteUserName"></div>
|
||||
<div class="small text-muted font-monospace" id="deleteUserEmail"></div>
|
||||
</div>
|
||||
<p class="text-muted small mb-0"><i class="bi bi-exclamation-triangle me-1 text-warning"></i>{% trans %}Esta acción desvinculará sus comisiones y reservas asociadas. No se puede deshacer.{% endtrans %}</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans %}Cancelar{% endtrans %}</button>
|
||||
<form id="deleteUserForm" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger px-3">
|
||||
<i class="bi bi-trash me-1"></i>{% trans %}Eliminar Usuario{% endtrans %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var deleteModalEl = document.getElementById('deleteUserModal');
|
||||
var deleteModal = deleteModalEl ? new bootstrap.Modal(deleteModalEl) : null;
|
||||
var nameEl = document.getElementById('deleteUserName');
|
||||
var emailEl = document.getElementById('deleteUserEmail');
|
||||
var formEl = document.getElementById('deleteUserForm');
|
||||
|
||||
document.querySelectorAll('.btn-delete-user').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var name = this.getAttribute('data-user-name');
|
||||
var email = this.getAttribute('data-user-email');
|
||||
var actionUrl = this.getAttribute('data-action-url');
|
||||
if (nameEl) nameEl.textContent = name;
|
||||
if (emailEl) emailEl.textContent = email;
|
||||
if (formEl) formEl.action = actionUrl;
|
||||
if (deleteModal) deleteModal.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
<li><a class="dropdown-item" href="{{ url_for('schedule.today_schedule') }}">
|
||||
<i class="bi bi-calendar-today"></i> {% trans %}Today's Schedule{% endtrans %}
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('schedule.commissions_view') }}">
|
||||
<i class="bi bi-diagram-3 me-2 text-primary"></i>{% trans %}Comisiones y Cursadas{% endtrans %}
|
||||
</a></li>
|
||||
{% if current_user.has_permission('optimizer', 'read') or (current_user.role and current_user.role|upper == 'ADMIN') %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('genetic_algorithm_web.genetic_optimizer') }}">
|
||||
@@ -106,6 +109,7 @@
|
||||
<ul class="dropdown-menu shadow-sm">
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin.users_list') }}"><i class="bi bi-people me-2"></i>{% trans %}Users{% endtrans %}</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin.roles_list') }}"><i class="bi bi-person-badge me-2"></i>{% trans %}Roles & Groups{% endtrans %}</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('schedule.commissions_view') }}"><i class="bi bi-diagram-3 me-2 text-primary"></i>{% trans %}Comisiones{% endtrans %}</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('buildings.list_buildings') }}"><i class="bi bi-buildings me-2"></i>{% trans %}Buildings{% endtrans %}</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin.milestone_types_list') }}"><i class="bi bi-tags me-2 text-warning"></i>{% trans %}Tipificaciones{% endtrans %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -97,8 +97,8 @@ def init_database():
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Operador',
|
||||
'description': 'Gestión operativa de aulas, sedes, reservas, importación y cartelera',
|
||||
'name': 'Bedelía',
|
||||
'description': 'Gestión operativa de aulas, sedes, reservas, comisiones, importación y cartelera',
|
||||
'is_system': False,
|
||||
'permissions': {
|
||||
'classrooms': 'read_write',
|
||||
@@ -113,7 +113,7 @@ def init_database():
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Consulta',
|
||||
'name': 'Alumno',
|
||||
'description': 'Acceso de solo lectura al cronograma general, asignaturas y aulas',
|
||||
'is_system': False,
|
||||
'permissions': {
|
||||
|
||||
Reference in New Issue
Block a user