feat: Evaluar proyectos

parent 98edc543
Pipeline #572 failed with stage
in 1 second
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Criterio, Opcion
# Register your models here.
admin.site.register(Criterio)
admin.site.register(Opcion)
admin.site.site_header = _('Administración de Manhattan')
admin.site.site_title = _('Administración de Manhattan')
admin.site.index_title = _('Inicio')
# Generated by Django 3.0.6 on 2020-06-11 08:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('indo', '0007_auto_20200528_1143'),
]
operations = [
migrations.CreateModel(
name='Criterio',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('parte', models.PositiveSmallIntegerField(verbose_name='parte')),
('peso', models.PositiveSmallIntegerField(verbose_name='peso')),
('descripcion', models.CharField(max_length=255)),
('tipo', models.CharField(choices=[('opcion', 'Opción'), ('texto', 'Texto libre')], max_length=15, verbose_name='tipo')),
('convocatoria', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Convocatoria')),
],
options={
'ordering': ('convocatoria', 'parte', 'peso'),
},
),
migrations.CreateModel(
name='Opcion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('puntuacion', models.PositiveSmallIntegerField(verbose_name='puntuación')),
('descripcion', models.CharField(max_length=255, verbose_name='descripción')),
('criterio', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='opciones', to='indo.Criterio')),
],
options={
'verbose_name': 'opción',
'verbose_name_plural': 'opciones',
'ordering': ('criterio__parte', 'criterio__peso', 'puntuacion'),
},
),
migrations.AlterField(
model_name='estudio',
name='rama',
field=models.CharField(choices=[('B', 'Formación básica sin rama'), ('H', 'Artes y Humanidades'), ('J', 'Ciencias Sociales y Jurídicas'), ('P', 'Títulos Propios'), ('S', 'Ciencias de la Salud'), ('T', 'Ingeniería y Arquitectura'), ('X', 'Ciencias')], max_length=1, verbose_name='rama'),
),
migrations.CreateModel(
name='Valoracion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('texto', models.TextField(blank=True, null=True, verbose_name='texto')),
('criterio', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Criterio')),
('opcion', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='indo.Opcion')),
('proyecto', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='valoraciones', to='indo.Proyecto')),
],
options={
'verbose_name': 'valoración',
'verbose_name_plural': 'valoraciones',
},
),
]
......@@ -59,6 +59,34 @@ class Convocatoria(models.Model):
return str(self.id)
class Criterio(models.Model):
"""Criterios para evaluar proyectos por la ACPUA."""
class Tipo(models.TextChoices):
"""Tipo de criterio.
Los criterios pueden ser de dos tipos:
* Opción - Se debe elegir una de las opciones predefinidas, con su puntuación asignada.
* Texto libre - El evaluador puede introducir un texto con sus comentarios.
"""
OPCION = 'opcion', _('Opción')
TEXTO = 'texto', _('Texto libre')
convocatoria = models.ForeignKey('Convocatoria', on_delete=models.PROTECT)
parte = models.PositiveSmallIntegerField(_('parte'))
peso = models.PositiveSmallIntegerField(_('peso'))
descripcion = models.CharField(max_length=255)
tipo = models.CharField(_('tipo'), max_length=15, choices=Tipo.choices)
class Meta:
ordering = ('convocatoria', 'parte', 'peso')
def __str__(self):
return self.descripcion
class Departamento(models.Model):
id = models.AutoField(primary_key=True)
academico_id_nk = models.IntegerField('cód. académico', blank=True, db_index=True, null=True)
......@@ -82,18 +110,18 @@ class Departamento(models.Model):
class Estudio(models.Model):
OPCIONES_RAMA = (
('B', 'Formación básica sin rama'),
('H', 'Artes y Humanidades'),
('J', 'Ciencias Sociales y Jurídicas'),
('P', 'Títulos Propios'),
('S', 'Ciencias de la Salud'),
('T', 'Ingeniería y Arquitectura'),
('X', 'Ciencias'),
('B', _('Formación básica sin rama')),
('H', _('Artes y Humanidades')),
('J', _('Ciencias Sociales y Jurídicas')),
('P', _('Títulos Propios')),
('S', _('Ciencias de la Salud')),
('T', _('Ingeniería y Arquitectura')),
('X', _('Ciencias')),
)
id = models.PositiveSmallIntegerField(_('Cód. estudio'), primary_key=True)
nombre = models.CharField(max_length=255)
esta_activo = models.BooleanField(_('¿Activo?'), default=True)
rama = models.CharField(max_length=1, choices=OPCIONES_RAMA)
rama = models.CharField(_('rama'), max_length=1, choices=OPCIONES_RAMA)
tipo_estudio = models.ForeignKey('TipoEstudio', on_delete=models.PROTECT)
class Meta:
......@@ -445,33 +473,53 @@ class Proyecto(models.Model):
return None
def get_coordinador(self):
'''Devuelve el usuario coordinador del proyecto'''
"""Devuelve el usuario coordinador del proyecto"""
coordinador = self.get_participante_or_none('coordinador')
return coordinador.usuario if coordinador else None
def get_coordinador_2(self):
'''Devuelve el segundo coordinador del proyecto (los PIET pueden tener 2).'''
"""Devuelve el segundo coordinador del proyecto (los PIET pueden tener 2)."""
coordinador_2 = self.get_participante_or_none('coordinador_2')
return coordinador_2.usuario if coordinador_2 else None
def get_coordinadores(self):
'''Devuelve los usuarios coordinadores del proyecto.'''
"""Devuelve los usuarios coordinadores del proyecto."""
coordinadores = [self.get_coordinador(), self.get_coordinador_2()]
return list(filter(None, coordinadores))
def get_dict_valoraciones(self):
"""Devuelve un diccionario criterio_id => valoración con las valoraciones del proyecto."""
return {valoracion.criterio_id: valoracion for valoracion in self.valoraciones.all()}
def get_usuarios_vinculados(self):
'''
"""
Devuelve todos los usuarios vinculados al proyecto
(invitados, participantes, etc).
'''
"""
return list(map(lambda p: p.usuario, self.participantes.all()))
def tiene_invitados(self):
'''Devuelve si el proyecto tiene al menos un invitado.'''
"""Devuelve si el proyecto tiene al menos un invitado."""
num_invitados = self.participantes.filter(tipo_participacion='invitado').count()
return num_invitados >= 1
class Opcion(models.Model):
"""Respuestas posibles a los criterios, cada una con una puntuación."""
criterio = models.ForeignKey('Criterio', on_delete=models.PROTECT, related_name='opciones')
puntuacion = models.PositiveSmallIntegerField(_('puntuación'))
descripcion = models.CharField(_('descripción'), max_length=255)
class Meta:
ordering = ('criterio__parte', 'criterio__peso', 'puntuacion')
verbose_name = _('opción')
verbose_name_plural = _('opciones')
def __str__(self):
return self.descripcion
class Registro(models.Model):
fecha = models.DateTimeField(auto_now_add=True)
descripcion = models.CharField(max_length=255)
......@@ -486,3 +534,16 @@ class TipoEstudio(models.Model):
class TipoParticipacion(models.Model):
nombre = models.CharField(primary_key=True, max_length=63)
class Valoracion(models.Model):
"""Valoración de un proyecto, atendiendo a los criterios establecidos en la convocatoria."""
proyecto = models.ForeignKey('Proyecto', on_delete=models.PROTECT, related_name='valoraciones')
criterio = models.ForeignKey('Criterio', on_delete=models.PROTECT)
opcion = models.ForeignKey('Opcion', null=True, on_delete=models.PROTECT)
texto = models.TextField(_('texto'), blank=True, null=True)
class Meta:
verbose_name = _('valoración')
verbose_name_plural = _('valoraciones')
......@@ -43,7 +43,7 @@ class ProyectosEvaluadosTable(tables.Table):
boton_evaluar = tables.Column(empty_values=(), orderable=False, verbose_name='')
def render_boton_evaluar(self, record):
enlace = reverse('proyecto_detail', args=[record.id]) # FIXME Hacer página de evaluación
enlace = reverse('evaluacion', args=[record.id])
return mark_safe(
f'''<a href="{enlace}" title={_("Evaluar el proyecto")}
aria-label={_('Evaluar el proyecto')} class="btn btn-info btn-sm">
......
......@@ -41,28 +41,34 @@ def alert_style(tag):
@register.simple_tag
def lord_url():
'''Devuelve la URL del Single Sign On.'''
"""Devuelve la URL del Single Sign On."""
return '{base}?{params}'.format(
base=reverse('social:begin', kwargs={'backend': 'saml'}),
params=urllib.parse.urlencode({'next': '/', 'idp': 'lord'}),
)
@register.filter
def get_item(dictionary, key):
"""Devuelve el valor de la clave `key` en el diccionario `dictionary`."""
return dictionary.get(key)
@register.filter
def get_obj_attr(obj, attr):
'''Devuelve el valor del atributo `attr` del objeto `obj`.'''
"""Devuelve el valor del atributo `attr` del objeto `obj`."""
return getattr(obj, attr)
@register.filter
def get_attr_verbose_name(obj, attr):
'''Devuelve el nombre prolijo del atributo indicado.'''
"""Devuelve el nombre prolijo del atributo indicado."""
return obj._meta.get_field(attr).verbose_name
@register.filter(name='has_group')
def has_group(user, group_name):
'''Comprueba si el usuario pertenece al grupo indicado.'''
"""Comprueba si el usuario pertenece al grupo indicado."""
return user.groups.filter(name=group_name).exists()
......
......@@ -4,6 +4,7 @@ from django.urls import include, path
from .views import (
AyudaView,
EvaluacionView,
HomePageView,
InvitacionView,
ParticipanteAceptarView,
......@@ -32,6 +33,7 @@ urlpatterns = [
ProyectosEvaluadosTableView.as_view(),
name='proyectos_evaluados_table',
),
path('evaluador/<int:pk>/evaluacion/', EvaluacionView.as_view(), name='evaluacion'),
path('gestion/proyecto/<int:anyo>/', ProyectoTableView.as_view(), name='proyectos_table'),
path(
'gestion/proyecto/<int:anyo>/evaluadores',
......
......@@ -36,12 +36,14 @@ from .forms import EvaluadorForm, InvitacionForm, ProyectoForm
from .models import (
Centro,
Convocatoria,
Criterio,
Evento,
ParticipanteProyecto,
Plan,
Proyecto,
Registro,
TipoParticipacion,
Valoracion,
)
from .tables import EvaluadoresTable, ProyectosEvaluadosTable, ProyectosTable
......@@ -51,40 +53,41 @@ class ChecksMixin(UserPassesTestMixin):
def es_coordinador(self, proyecto_id):
"""Devuelve si el usuario actual es coordinador del proyecto indicado."""
self.permission_denied_message = _('Usted no es coordinador de este proyecto.')
proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
usuario_actual = self.request.user
coordinadores_participantes = proyecto.participantes.filter(
tipo_participacion__in=['coordinador', 'coordinador_2']
).all()
usuarios_coordinadores = list(map(lambda p: p.usuario, coordinadores_participantes))
self.permission_denied_message = _('Usted no es coordinador de este proyecto.')
return usuario_actual in usuarios_coordinadores
def es_participante(self, proyecto_id):
"""Devuelve si el usuario actual es participante del proyecto indicado."""
self.permission_denied_message = _('Usted no es participante de este proyecto.')
proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
usuario_actual = self.request.user
pp = proyecto.participantes.filter(
usuario=usuario_actual, tipo_participacion='participante'
).all()
self.permission_denied_message = _('Usted no es participante de este proyecto.')
return True if pp else False
def es_invitado(self, proyecto_id):
"""Devuelve si el usuario actual es invitado del proyecto indicado."""
self.permission_denied_message = _('Usted no está invitado a este proyecto.')
proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
usuario_actual = self.request.user
pp = proyecto.participantes.filter(
usuario=usuario_actual, tipo_participacion='invitado'
).all()
self.permission_denied_message = _('Usted no está invitado a este proyecto.')
return True if pp else False
def esta_vinculado(self, proyecto_id):
"""Devuelve si el usuario actual está vinculado al proyecto indicado."""
self.permission_denied_message = _('Usted no está vinculado a este proyecto.')
proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
usuario_actual = self.request.user
pp = (
......@@ -92,15 +95,14 @@ class ChecksMixin(UserPassesTestMixin):
.exclude(tipo_participacion='invitacion_rehusada')
.all()
)
self.permission_denied_message = _('Usted no está vinculado a este proyecto.')
return True if pp else False
def es_pas_o_pdi(self):
"""Devuelve si el usuario actual es PAS o PDI de la UZ o de sus centros adscritos."""
self.permission_denied_message = _('Usted no es PAS ni PDI.')
usuario_actual = self.request.user
colectivos_del_usuario = json.loads(usuario_actual.colectivos)
self.permission_denied_message = _('Usted no es PAS ni PDI.')
return any(
col_autorizado in colectivos_del_usuario for col_autorizado in ['PAS', 'ADS', 'PDI']
......@@ -108,14 +110,14 @@ class ChecksMixin(UserPassesTestMixin):
def es_decano_o_director(self, proyecto_id):
"""Devuelve si el usuario actual es decano/director del centro del proyecto."""
usuario_actual = self.request.user
self.permission_denied_message = _('Usted no es decano/director del centro del proyecto.')
proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
usuario_actual = self.request.user
centro = proyecto.centro
if not centro:
return False
nip_decano = centro.nip_decano
self.permission_denied_message = _('Usted no es decano/director del centro del proyecto.')
nip_decano = centro.nip_decano
return usuario_actual.username == str(nip_decano)
def esta_vinculado_o_es_decano_o_es_coordinador(self, proyecto_id):
......@@ -123,6 +125,11 @@ class ChecksMixin(UserPassesTestMixin):
Devuelve si el usuario actual está vinculado al proyecto indicado
o es decano o director del centro del proyecto
o es coordinador del plan de estudios del proyecto."""
self.permission_denied_message = _(
'Usted no está vinculado a este proyecto, '
'ni es decano/director del centro del proyecto, '
'ni es coordinador del plan de estudios del proyecto.'
)
usuario_actual = self.request.user
esta_autorizado = (
self.esta_vinculado(proyecto_id)
......@@ -130,35 +137,78 @@ class ChecksMixin(UserPassesTestMixin):
or self.es_coordinador_estudio(proyecto_id)
or usuario_actual.has_perm('indo.ver_proyecto') # Gestores y evaluadores
)
self.permission_denied_message = _(
'Usted no está vinculado a este proyecto, '
'ni es decano/director del centro del proyecto, '
'ni es coordinador del plan de estudios del proyecto.'
)
return esta_autorizado
def es_coordinador_estudio(self, proyecto_id):
"""Devuelve si el usuario actual es coordinador del estudio del proyecto."""
usuario_actual = self.request.user
self.permission_denied_message = _(
'Usted no es coordinador del plan de estudios del proyecto.'
)
proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
usuario_actual = self.request.user
estudio = proyecto.estudio
if not estudio:
return False
nip_coordinadores = [
f'{p.nip_coordinador}' for p in estudio.planes.all() if p.nip_coordinador
]
return usuario_actual.username in nip_coordinadores
self.permission_denied_message = _(
'Usted no es coordinador del plan de estudios del proyecto.'
)
def es_evaluador_del_proyecto(self, proyecto_id):
"""Devuelve si el usuario actual es evaluador del proyecto indicado."""
self.permission_denied_message = _('Usted no es evaluador de este proyecto.')
proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
usuario_actual = self.request.user
return usuario_actual.username in nip_coordinadores
return usuario_actual == proyecto.evaluador
class AyudaView(TemplateView):
template_name = 'ayuda.html'
class EvaluacionView(LoginRequiredMixin, ChecksMixin, TemplateView):
"""Muestra y permite editar las valoraciones de un proyecto."""
template_name = 'evaluador/evaluacion.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
proyecto = get_object_or_404(Proyecto, pk=kwargs['pk'])
context['proyecto'] = proyecto
context['criterios'] = Criterio.objects.filter(
convocatoria_id=proyecto.convocatoria_id
).all()
context['dict_valoraciones'] = proyecto.get_dict_valoraciones()
return context
def post(self, request, *args, **kwargs):
context = super().get_context_data(**kwargs)
proyecto = get_object_or_404(Proyecto, pk=kwargs['pk'])
criterios = Criterio.objects.filter(convocatoria_id=proyecto.convocatoria_id).all()
dict_valoraciones = proyecto.get_dict_valoraciones()
for criterio in criterios:
valoracion = dict_valoraciones.get(criterio.id)
if not valoracion:
valoracion = Valoracion(proyecto_id=proyecto.id, criterio_id=criterio.id)
if criterio.tipo == 'opcion':
valoracion.opcion_id = request.POST.get(str(criterio.id))
elif criterio.tipo == 'texto':
valoracion.texto = request.POST.get(str(criterio.id))
valoracion.save()
return redirect('evaluacion', proyecto.id)
def test_func(self):
return self.es_evaluador_del_proyecto(self.kwargs['pk'])
class ProyectoEvaluadorTableView(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView):
"""Muestra una tabla con las solicitudes de proyectos presentadas y el evaluador asignado."""
......
{% extends 'base.html' %}
{% load custom_tags i18n %}
{% block title %}{% trans "Evaluación" %}: {{ proyecto.titulo }}{% endblock title %}
{% block content %}
<div class='container-blanco'>
<h1 id="cabecera">{% trans "Evaluación" %} <small>{{ proyecto.titulo }}</small></h1>
<hr />
<br />
<div class="alert-info alert fade-in">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<span class="fas fa-info-circle"></span>
{% trans 'Aquí puede ver/editar la evaluación del proyecto.' %}
{% trans 'Pulse el botón «Guardar» para almacenar sus cambios.' %}
</div><br />
<form action="" method="post">
{% csrf_token %}
{% for criterio in criterios %}
<p><strong>{{ criterio.descripcion }}</strong></p>
{% with valoracion=dict_valoraciones|get_item:criterio.id %}
{% if criterio.tipo == 'opcion' %}
<select name="{{ criterio.id }}">
<option value="" {% if not valoracion %}selected{% endif %}>---------</option>
{% for opcion in criterio.opciones.all %}
<option value="{{ opcion.id }}" {% if opcion.id == valoracion.opcion_id %} selected {% endif %}>
{{ opcion.descripcion }}
</option>
{% endfor %}
</select>
{% elif criterio.tipo == 'texto' %}
<textarea class="textarea form-control" name="{{ criterio.id }}"
placeholder="{% trans 'Introduzca sus comentarios.' %}" rows="10"
cols="80">{% if valoracion %}{{ valoracion.texto }}{% endif %}</textarea>
{% endif %}
{% endwith %}
<p></p><br />
{% endfor %}
<button class="btn btn-primary" type="submit" title="{% trans 'Guardar evaluación' %}">
<span class="fas fa-check"></span> {% trans 'Guardar' %}
</button>
</form>
</div>
{% endblock content %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment