Compare commits
1
Commits
71fb20a271
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ebc5df4fd |
@@ -33,6 +33,8 @@ Registro de hitos y versiones correspondientes a las Fases del [ROADMAP_MVP.md](
|
|||||||
### Complementos y Seguridad Transversal
|
### Complementos y Seguridad Transversal
|
||||||
* **Dashboard de Bedelía:** Integración de paneles en tiempo real para análisis de **Comisiones Sin Reserva** y grilla de **Aulas Disponibles** (Filtros Semana/Mes) conectando frontend UI con el servicio analítico backend.
|
* **Dashboard de Bedelía:** Integración de paneles en tiempo real para análisis de **Comisiones Sin Reserva** y grilla de **Aulas Disponibles** (Filtros Semana/Mes) conectando frontend UI con el servicio analítico backend.
|
||||||
* **Auditoría Automatizada (Fase 1):** Implantación de un pipeline de seguridad local/Gitea que aísla dependencias de desarrollo (`requirements-dev.txt`) y ejecuta controles SAST, DAST y SCA (Bandit, pip-audit, njsscan y Schemathesis) sin vulnerabilidades detectadas.
|
* **Auditoría Automatizada (Fase 1):** Implantación de un pipeline de seguridad local/Gitea que aísla dependencias de desarrollo (`requirements-dev.txt`) y ejecuta controles SAST, DAST y SCA (Bandit, pip-audit, njsscan y Schemathesis) sin vulnerabilidades detectadas.
|
||||||
|
* **Optimizador Genético de Reservas:** Reparación de enrutamiento 404, migración a JWT (`@jwt_required`), corrección de UI de métricas dinámicas de algoritmo, y refactorización de layout para superposición de herramientas.
|
||||||
|
* **UI/UX General:** Ajustes de CSS en modo claro (soporte dinámico de contraste de isologotipo blanco a rosa institucional mediante filtros) y traducciones de menús de usuario en `base.html`.
|
||||||
---
|
---
|
||||||
|
|
||||||
## [Fase 1: Paridad Legacy y Estabilización] — v2.5.0 / v2.4.0 (2026-09-19)
|
## [Fase 1: Paridad Legacy y Estabilización] — v2.5.0 / v2.4.0 (2026-09-19)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ def create_app(config_class=Config):
|
|||||||
from app.routes.api.dashboard import api_dashboard_bp
|
from app.routes.api.dashboard import api_dashboard_bp
|
||||||
from app.routes.api.admin import api_admin_bp
|
from app.routes.api.admin import api_admin_bp
|
||||||
from app.routes.api.openapi import api_openapi_bp
|
from app.routes.api.openapi import api_openapi_bp
|
||||||
|
from app.routes.genetic_algorithm import genetic_bp
|
||||||
from app.security import SecurityFilterChain
|
from app.security import SecurityFilterChain
|
||||||
|
|
||||||
# Register API v1 blueprints
|
# Register API v1 blueprints
|
||||||
@@ -39,6 +40,7 @@ def create_app(config_class=Config):
|
|||||||
app.register_blueprint(api_optimizer_bp)
|
app.register_blueprint(api_optimizer_bp)
|
||||||
app.register_blueprint(api_dashboard_bp)
|
app.register_blueprint(api_dashboard_bp)
|
||||||
app.register_blueprint(api_openapi_bp)
|
app.register_blueprint(api_openapi_bp)
|
||||||
|
app.register_blueprint(genetic_bp)
|
||||||
app.register_blueprint(api_admin_bp, url_prefix='/api/v1')
|
app.register_blueprint(api_admin_bp, url_prefix='/api/v1')
|
||||||
app.register_blueprint(api_admin_bp, url_prefix='/api/v1/admin', name='api_admin_prefixed')
|
app.register_blueprint(api_admin_bp, url_prefix='/api/v1/admin', name='api_admin_prefixed')
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
from flask import Blueprint, request, jsonify, current_app, render_template
|
from flask import Blueprint, request, jsonify, current_app, render_template, g
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from app.models.genetic_algorithm import ReservationOptimizer
|
from app.models.genetic_algorithm import ReservationOptimizer
|
||||||
from app.models.reservation import Reservation, ReservationStatus
|
from app.models.reservation import Reservation, ReservationStatus
|
||||||
from app import db
|
from app import db
|
||||||
from flask_login import login_required, current_user
|
from app.utils.jwt_decorators import jwt_required
|
||||||
|
|
||||||
genetic_bp = Blueprint('genetic_algorithm', __name__, url_prefix='/api/genetic')
|
genetic_bp = Blueprint('genetic_algorithm', __name__, url_prefix='/api/genetic')
|
||||||
|
|
||||||
|
|
||||||
@genetic_bp.route('/optimize', methods=['POST'])
|
@genetic_bp.route('/optimize', methods=['POST'])
|
||||||
@login_required
|
@jwt_required
|
||||||
def optimize_reservations():
|
def optimize_reservations():
|
||||||
"""Optimize reservations using genetic algorithm"""
|
"""Optimize reservations using genetic algorithm"""
|
||||||
try:
|
try:
|
||||||
@@ -40,14 +40,14 @@ def optimize_reservations():
|
|||||||
return jsonify({'error': 'Invalid end_date format. Use ISO format.'}), 400
|
return jsonify({'error': 'Invalid end_date format. Use ISO format.'}), 400
|
||||||
|
|
||||||
# Check if user has permission (admin, teacher or RBAC optimizer write)
|
# Check if user has permission (admin, teacher or RBAC optimizer write)
|
||||||
if not (current_user.is_admin() or current_user.has_permission('optimizer', 'read_write') or current_user.role in ['ADMIN', 'teacher']):
|
if not (g.jwt_user.is_admin() or g.jwt_user.has_permission('optimizer', 'read_write') or g.jwt_user.role in ['ADMIN', 'teacher']):
|
||||||
return jsonify({'error': 'Insufficient permissions to optimize reservations'}), 403
|
return jsonify({'error': 'Insufficient permissions to optimize reservations'}), 403
|
||||||
|
|
||||||
# Initialize optimizer and run optimization
|
# Initialize optimizer and run optimization
|
||||||
optimizer = ReservationOptimizer()
|
optimizer = ReservationOptimizer()
|
||||||
result = optimizer.optimize_schedule(
|
result = optimizer.optimize_schedule(
|
||||||
commission_ids=commission_ids,
|
commission_ids=commission_ids,
|
||||||
admin_user_id=current_user.id,
|
admin_user_id=g.jwt_user.id,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date
|
end_date=end_date
|
||||||
)
|
)
|
||||||
@@ -69,7 +69,7 @@ def optimize_reservations():
|
|||||||
|
|
||||||
|
|
||||||
@genetic_bp.route('/apply-optimization', methods=['POST'])
|
@genetic_bp.route('/apply-optimization', methods=['POST'])
|
||||||
@login_required
|
@jwt_required
|
||||||
def apply_optimization():
|
def apply_optimization():
|
||||||
"""Apply optimized reservations to the database"""
|
"""Apply optimized reservations to the database"""
|
||||||
try:
|
try:
|
||||||
@@ -84,7 +84,7 @@ def apply_optimization():
|
|||||||
return jsonify({'error': 'reservations must be a list'}), 400
|
return jsonify({'error': 'reservations must be a list'}), 400
|
||||||
|
|
||||||
# Check if user has permission
|
# Check if user has permission
|
||||||
if not (current_user.is_admin() or current_user.has_permission('optimizer', 'read_write') or current_user.role in ['ADMIN', 'teacher']):
|
if not (g.jwt_user.is_admin() or g.jwt_user.has_permission('optimizer', 'read_write') or g.jwt_user.role in ['ADMIN', 'teacher']):
|
||||||
return jsonify({'error': 'Insufficient permissions to apply reservations'}), 403
|
return jsonify({'error': 'Insufficient permissions to apply reservations'}), 403
|
||||||
|
|
||||||
# Initialize optimizer and apply reservations
|
# Initialize optimizer and apply reservations
|
||||||
@@ -106,7 +106,7 @@ def apply_optimization():
|
|||||||
|
|
||||||
|
|
||||||
@genetic_bp.route('/preview-optimization', methods=['POST'])
|
@genetic_bp.route('/preview-optimization', methods=['POST'])
|
||||||
@login_required
|
@jwt_required
|
||||||
def preview_optimization():
|
def preview_optimization():
|
||||||
"""Preview optimization without applying to database"""
|
"""Preview optimization without applying to database"""
|
||||||
try:
|
try:
|
||||||
@@ -124,14 +124,14 @@ def preview_optimization():
|
|||||||
return jsonify({'error': 'commission_ids must be a non-empty list'}), 400
|
return jsonify({'error': 'commission_ids must be a non-empty list'}), 400
|
||||||
|
|
||||||
# Check if user has permission
|
# Check if user has permission
|
||||||
if current_user.role not in ['ADMIN', 'teacher']:
|
if g.jwt_user.role not in ['ADMIN', 'teacher']:
|
||||||
return jsonify({'error': 'Insufficient permissions to preview optimization'}), 403
|
return jsonify({'error': 'Insufficient permissions to preview optimization'}), 403
|
||||||
|
|
||||||
# Initialize optimizer and run optimization
|
# Initialize optimizer and run optimization
|
||||||
optimizer = ReservationOptimizer()
|
optimizer = ReservationOptimizer()
|
||||||
result = optimizer.optimize_schedule(
|
result = optimizer.optimize_schedule(
|
||||||
commission_ids=commission_ids,
|
commission_ids=commission_ids,
|
||||||
admin_user_id=current_user.id
|
admin_user_id=g.jwt_user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add additional preview information
|
# Add additional preview information
|
||||||
@@ -166,12 +166,12 @@ def preview_optimization():
|
|||||||
|
|
||||||
|
|
||||||
@genetic_bp.route('/commissions', methods=['GET'])
|
@genetic_bp.route('/commissions', methods=['GET'])
|
||||||
@login_required
|
@jwt_required
|
||||||
def get_commissions():
|
def get_commissions():
|
||||||
"""Get available commissions for optimization"""
|
"""Get available commissions for optimization"""
|
||||||
try:
|
try:
|
||||||
# Check if user has permission
|
# Check if user has permission
|
||||||
if current_user.role not in ['ADMIN', 'teacher']:
|
if g.jwt_user.role not in ['ADMIN', 'teacher']:
|
||||||
return jsonify({'error': 'Insufficient permissions'}), 403
|
return jsonify({'error': 'Insufficient permissions'}), 403
|
||||||
|
|
||||||
from app.models.subject import Commission, Subject
|
from app.models.subject import Commission, Subject
|
||||||
@@ -196,12 +196,12 @@ def get_commissions():
|
|||||||
|
|
||||||
|
|
||||||
@genetic_bp.route('/algorithm-status', methods=['GET'])
|
@genetic_bp.route('/algorithm-status', methods=['GET'])
|
||||||
@login_required
|
@jwt_required
|
||||||
def get_algorithm_status():
|
def get_algorithm_status():
|
||||||
"""Get genetic algorithm configuration status"""
|
"""Get genetic algorithm configuration status"""
|
||||||
try:
|
try:
|
||||||
# Check if user has permission
|
# Check if user has permission
|
||||||
if current_user.role not in ['ADMIN']:
|
if g.jwt_user.role not in ['ADMIN']:
|
||||||
return jsonify({'error': 'Insufficient permissions'}), 403
|
return jsonify({'error': 'Insufficient permissions'}), 403
|
||||||
|
|
||||||
# Get system status
|
# Get system status
|
||||||
@@ -246,10 +246,10 @@ genetic_web_bp = Blueprint('genetic_algorithm_web', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@genetic_web_bp.route('/genetic-optimizer')
|
@genetic_web_bp.route('/genetic-optimizer')
|
||||||
@login_required
|
@jwt_required
|
||||||
def genetic_optimizer():
|
def genetic_optimizer():
|
||||||
"""Render the genetic algorithm optimizer interface"""
|
"""Render the genetic algorithm optimizer interface"""
|
||||||
if not (current_user.has_permission('optimizer', 'read') or (current_user.role and current_user.role.upper() == 'ADMIN')):
|
if not (g.jwt_user.has_permission('optimizer', 'read') or (g.jwt_user.role and g.jwt_user.role.upper() == 'ADMIN')):
|
||||||
return render_template('auth/403.html'), 403
|
return render_template('auth/403.html'), 403
|
||||||
|
|
||||||
return render_template('genetic_optimizer.html')
|
return render_template('genetic_optimizer.html')
|
||||||
@@ -42,6 +42,10 @@
|
|||||||
background: #F8FAFC !important;
|
background: #F8FAFC !important;
|
||||||
color: #1E293B !important;
|
color: #1E293B !important;
|
||||||
}
|
}
|
||||||
|
[data-bs-theme="light"] .navbar-logo {
|
||||||
|
/* Filtro para convertir blanco a rosa (#B43E8E) */
|
||||||
|
filter: brightness(0) saturate(100%) invert(35%) sepia(55%) saturate(2250%) hue-rotate(289deg) brightness(90%) contrast(90%);
|
||||||
|
}
|
||||||
[data-bs-theme="light"] .glass-navbar {
|
[data-bs-theme="light"] .glass-navbar {
|
||||||
background: rgba(255, 255, 255, 0.88) !important;
|
background: rgba(255, 255, 255, 0.88) !important;
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
@@ -410,7 +414,7 @@
|
|||||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||||
<li>
|
<li>
|
||||||
<div class="px-3 py-2 border-bottom">
|
<div class="px-3 py-2 border-bottom">
|
||||||
<small class="text-muted d-block">Signed in as</small>
|
<small class="text-muted d-block">Sesión iniciada como</small>
|
||||||
<span class="fw-bold small d-block">{{ user.email }}</span>
|
<span class="fw-bold small d-block">{{ user.email }}</span>
|
||||||
<span class="badge {{ user.role_badge_display.class }} role-pill mt-1">
|
<span class="badge {{ user.role_badge_display.class }} role-pill mt-1">
|
||||||
<i class="bi {{ user.role_badge_display.icon }}"></i> {{ user.role_badge_display.label }}
|
<i class="bi {{ user.role_badge_display.icon }}"></i> {{ user.role_badge_display.label }}
|
||||||
@@ -418,14 +422,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li><a class="dropdown-item" href="/auth/profile">
|
<li><a class="dropdown-item" href="/auth/profile">
|
||||||
<i class="bi bi-person me-2"></i> Profile
|
<i class="bi bi-person me-2"></i> Mi Perfil
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a class="dropdown-item" href="/auth/change_password">
|
<li><a class="dropdown-item" href="/auth/change_password">
|
||||||
<i class="bi bi-key me-2"></i> Change Password
|
<i class="bi bi-key me-2"></i> Cambiar Contraseña
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item text-danger" href="/auth/logout">
|
<li><a class="dropdown-item text-danger" href="/auth/logout">
|
||||||
<i class="bi bi-box-arrow-right me-2"></i> Logout
|
<i class="bi bi-box-arrow-right me-2"></i> Cerrar Sesión
|
||||||
</a></li>
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -83,7 +83,8 @@
|
|||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<!-- Algorithm Status Card -->
|
<!-- Algorithm Status Card -->
|
||||||
<div class="col-lg-4 mb-4">
|
<div class="col-lg-4 mb-4">
|
||||||
<div class="card border-0 shadow-sm algorithm-status">
|
<div class="algorithm-status" style="z-index: 10;">
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
<div class="card-header bg-body-tertiary border-0 py-3">
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||||
<h5 class="mb-0 h6 fw-bold">
|
<h5 class="mb-0 h6 fw-bold">
|
||||||
<i class="bi bi-gear-wide-connected text-primary me-2"></i>
|
<i class="bi bi-gear-wide-connected text-primary me-2"></i>
|
||||||
@@ -123,14 +124,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-primary w-100" onclick="geneticManager.getAlgorithmStatus()">
|
<button class="btn btn-sm btn-outline-primary w-100" onclick="window.refreshStatus()">
|
||||||
<i class="bi bi-arrow-clockwise me-1"></i>
|
<i class="bi bi-arrow-clockwise me-1"></i>
|
||||||
Refresh Status
|
Refresh Status
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<!-- Optimization Controls -->
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||||
|
<h5 class="mb-0 h6 fw-bold">
|
||||||
|
<i class="bi bi-sliders text-primary me-2"></i>
|
||||||
|
Optimization Controls
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="start-date" class="form-label">Start Date</label>
|
||||||
|
<input type="datetime-local" class="form-control" id="start-date" name="start_date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="end-date" class="form-label">End Date</label>
|
||||||
|
<input type="datetime-local" class="form-control" id="end-date" name="end_date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-info text-white" id="preview-optimization-btn">
|
||||||
|
<i class="bi bi-eye me-1"></i>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" id="optimize-assignments-btn">
|
||||||
|
<i class="bi bi-play-circle me-1"></i>
|
||||||
|
Run Full Optimization
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning" id="apply-optimization-btn" style="display: none;">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="resetOptimization()">
|
||||||
|
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Commission Selection -->
|
<!-- Commission Selection -->
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
@@ -161,54 +208,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Optimization Controls -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border-0 shadow-sm">
|
|
||||||
<div class="card-header bg-body-tertiary border-0 py-3">
|
|
||||||
<h5 class="mb-0 h6 fw-bold">
|
|
||||||
<i class="bi bi-sliders text-primary me-2"></i>
|
|
||||||
Optimization Controls
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row align-items-center mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="start-date" class="form-label">Start Date Range</label>
|
|
||||||
<input type="datetime-local" class="form-control" id="start-date" name="start_date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="end-date" class="form-label">End Date Range</label>
|
|
||||||
<input type="datetime-local" class="form-control" id="end-date" name="end_date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
|
||||||
<button class="btn btn-info text-white" id="preview-optimization-btn">
|
|
||||||
<i class="bi bi-eye me-1"></i>
|
|
||||||
Preview Optimization
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-success" id="optimize-assignments-btn">
|
|
||||||
<i class="bi bi-play-circle me-1"></i>
|
|
||||||
Run Full Optimization
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-warning" id="apply-optimization-btn" style="display: none;">
|
|
||||||
<i class="bi bi-check-circle me-1"></i>
|
|
||||||
Apply Reservations
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" onclick="resetOptimization()">
|
|
||||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results Section -->
|
<!-- Results Section -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
@@ -292,11 +292,25 @@ function resetOptimization() {
|
|||||||
document.getElementById('end-date').value = '';
|
document.getElementById('end-date').value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.refreshStatus = async function() {
|
||||||
|
if (window.geneticManager) {
|
||||||
|
try {
|
||||||
|
const statusResult = await geneticManager.getAlgorithmStatus();
|
||||||
|
if (statusResult && statusResult.success && statusResult.data) {
|
||||||
|
const sysStats = statusResult.data.system_status;
|
||||||
|
document.getElementById('available-classrooms').textContent = sysStats.available_classrooms;
|
||||||
|
document.getElementById('active-commissions').textContent = sysStats.active_commissions;
|
||||||
|
document.getElementById('existing-reservations').textContent = sysStats.confirmed_reservations;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error refreshing status:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
await loadCommissions();
|
await loadCommissions();
|
||||||
if (window.geneticManager) {
|
await window.refreshStatus();
|
||||||
await geneticManager.getAlgorithmStatus();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadCommissions() {
|
async function loadCommissions() {
|
||||||
|
|||||||
Reference in New Issue
Block a user