feat Visto bueno del coordinador del plan de estudios

En la implementación anterior los PIET debían ser solicitados por
un coordinador del estudio, quien podía nombrar un segundo coordinador.

Olga comunica un cambio de criterio:

He hablado con Carmen, y tanto por las dudas que nos planteaba JM
como por las que nosotras teníamos, hemos pensado que los PIET se traten
como los PIEC y PRACUZ, con un único coordinador y con el visto bueno
del coordinador de la titulación.
Independientemente de que en algunos casos sea el propio coordinador
de la titulación el que lo pida y lo coordine.
parent 8d99bda9
Pipeline #523 failed with stage
in 0 seconds
......@@ -54,7 +54,8 @@
"max_estudiantes": 2,
"campos": "[\"contexto\", \"objetivos\", \"metodos_estudio\", \"mejoras\", \"continuidad\", \"tipo\", \"contexto_aplicacion\", \"metodos\", \"tecnologias\", \"aplicacion\", \"proyectos_anteriores\", \"impacto\", \"innovacion\", \"interes\"]",
"convocatoria": 2019,
"requiere_visto_bueno": false
"requiere_visto_bueno_centro": false,
"requiere_visto_bueno_estudio": false
}
},
{
......@@ -67,7 +68,8 @@
"max_estudiantes": 2,
"campos": "[\"seminario\", \"idioma\", \"ramas\", \"ambito\", \"contenidos\", \"formatos\", \"material_previo\"]",
"convocatoria": 2019,
"requiere_visto_bueno": false
"requiere_visto_bueno_centro": false,
"requiere_visto_bueno_estudio": false
}
},
{
......@@ -80,7 +82,8 @@
"max_estudiantes": 2,
"campos": "[\"contexto\", \"objetivos\", \"metodos_estudio\", \"mejoras\", \"continuidad\", \"enlace\", \"tipo\", \"contexto_aplicacion\", \"metodos\", \"tecnologias\", \"aplicacion\", \"proyectos_anteriores\", \"impacto\", \"innovacion\", \"interes\"]",
"convocatoria": 2019,
"requiere_visto_bueno": false
"requiere_visto_bueno_centro": false,
"requiere_visto_bueno_estudio": true
}
},
{
......@@ -93,7 +96,8 @@
"max_estudiantes": 2,
"campos": "[\"contexto\", \"objetivos\", \"metodos_estudio\", \"mejoras\", \"continuidad\", \"tipo\", \"contexto_aplicacion\", \"metodos\", \"tecnologias\", \"aplicacion\", \"proyectos_anteriores\", \"impacto\", \"innovacion\", \"interes\"]",
"convocatoria": 2019,
"requiere_visto_bueno": true
"requiere_visto_bueno_centro": true,
"requiere_visto_bueno_estudio": false
}
},
{
......@@ -106,7 +110,8 @@
"max_estudiantes": 2,
"campos": "[\"justificacion_equipo\", \"objetivos\", \"caracter_estrategico\", \"ramas\", \"afectadas\", \"contenido_modulos\", \"duracion\", \"multimedia\", \"indicadores\", \"actividades\", \"material_previo\"]",
"convocatoria": 2019,
"requiere_visto_bueno": true
"requiere_visto_bueno_centro": true,
"requiere_visto_bueno_estudio": false
}
},
{
......@@ -119,7 +124,8 @@
"max_estudiantes": null,
"campos": "[\"contexto\", \"objetivos\", \"metodos_estudio\", \"mejoras\", \"continuidad\", \"tipo\", \"contexto_aplicacion\", \"metodos\", \"tecnologias\", \"aplicacion\", \"proyectos_anteriores\", \"impacto\", \"innovacion\", \"interes\"]",
"convocatoria": 2019,
"requiere_visto_bueno": false
"requiere_visto_bueno_centro": false,
"requiere_visto_bueno_estudio": false
}
},
{ "model": "indo.tipoparticipacion", "pk": "coordinador", "fields": {} },
......
......@@ -132,22 +132,6 @@ class ProyectoForm(forms.ModelForm):
"estudio", _("Los PIET deben estar vinculados a un estudio.")
)
if estudio:
nip_coordinadores = [
f"{p.nip_coordinador}"
for p in estudio.planes.all()
if p.nip_coordinador
]
if (
programa.nombre_corto == "PIET"
and self.instance.user.username not in nip_coordinadores
):
self.add_error(
"estudio",
_("Los PIET sólo los pueden solicitar los coordinadores del estudio."),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["programa"].widget.choices = tuple(
......
# Generated by Django 3.0.2 on 2020-02-03 12:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("indo", "0004_auto_20200131_1409")]
operations = [
migrations.AlterModelOptions(
name="proyecto",
options={
"permissions": [
(
"listar_proyectos",
"Puede ver el listado de todos los proyectos.",
),
("ver_proyecto", "Puede ver cualquier proyecto."),
]
},
),
migrations.RenameField(
model_name="programa",
old_name="requiere_visto_bueno",
new_name="requiere_visto_bueno_centro",
),
migrations.RemoveField(model_name="proyecto", name="visto_bueno"),
migrations.AddField(
model_name="programa",
name="requiere_visto_bueno_estudio",
field=models.BooleanField(
default="False",
verbose_name="¿Requiere el visto bueno del coordinador del plan de estudios?",
),
),
migrations.AddField(
model_name="proyecto",
name="visto_bueno_centro",
field=models.BooleanField(null=True, verbose_name="Visto bueno del centro"),
),
migrations.AddField(
model_name="proyecto",
name="visto_bueno_estudio",
field=models.BooleanField(
null=True, verbose_name="Visto bueno del plan de estudios"
),
),
]
......@@ -209,9 +209,13 @@ class Programa(models.Model):
)
campos = models.TextField(null=True)
convocatoria = models.ForeignKey("Convocatoria", on_delete=models.PROTECT)
requiere_visto_bueno = models.BooleanField(
requiere_visto_bueno_centro = models.BooleanField(
_("¿Requiere el visto bueno del director o decano?"), default="False"
)
requiere_visto_bueno_estudio = models.BooleanField(
_("¿Requiere el visto bueno del coordinador del plan de estudios?"),
default="False",
)
def __str__(self):
return f"{self.nombre_corto}"
......@@ -470,7 +474,10 @@ class Proyecto(models.Model):
help_text=_("En su caso."),
)
programa = models.ForeignKey("Programa", on_delete=models.PROTECT)
visto_bueno = models.BooleanField(_("Visto bueno"), null=True)
visto_bueno_centro = models.BooleanField(_("Visto bueno del centro"), null=True)
visto_bueno_estudio = models.BooleanField(
_("Visto bueno del plan de estudios"), null=True
)
class Meta:
permissions = [
......
......@@ -28,6 +28,7 @@ from .models import (
Convocatoria,
Evento,
ParticipanteProyecto,
Plan,
Proyecto,
Registro,
TipoParticipacion,
......@@ -114,22 +115,42 @@ class ChecksMixin(UserPassesTestMixin):
return usuario_actual.username == str(nip_decano)
def esta_vinculado_o_es_decano(self, proyecto_id):
def esta_vinculado_o_es_decano_o_es_coordinador(self, proyecto_id):
"""
Devuelve si el usuario actual está vinculado al proyecto indicado
o es decano o director del centro del proyecto."""
o es decano o director del centro del proyecto
o es coordinador del plan de estudios del proyecto."""
usuario_actual = self.request.user
esta_autorizado = (
self.esta_vinculado(proyecto_id)
or self.es_decano_o_director(proyecto_id)
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 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
proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
estudio = proyecto.estudio
if not estudio:
return False
nip_coordinadores = [
f"{p.nip_coordinador}" for p in estudio.planes.all() if p.nip_coordinador
]
self.permission_denied_message = _(
"Usted no es coordinador del plan de estudios del proyecto."
)
return usuario_actual.username in nip_coordinadores
class AyudaView(TemplateView):
template_name = "ayuda.html"
......@@ -386,7 +407,7 @@ class ProyectoDetailView(LoginRequiredMixin, ChecksMixin, DetailView):
def test_func(self):
proyecto_id = self.kwargs["pk"]
return self.esta_vinculado_o_es_decano(proyecto_id)
return self.esta_vinculado_o_es_decano_o_es_coordinador(proyecto_id)
class ProyectoListView(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView):
......@@ -468,8 +489,13 @@ class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView):
return super().post(request, *args, **kwargs)
self._enviar_invitaciones(request, proyecto)
if proyecto.programa.nombre_corto in ["PIEC", "PRACUZ"]:
self._enviar_solicitudes_visto_bueno(request, proyecto)
if proyecto.programa.requiere_visto_bueno_centro:
self._enviar_solicitudes_visto_bueno_centro(request, proyecto)
if proyecto.programa.requiere_visto_bueno_estudio:
self._enviar_solicitudes_visto_bueno_estudio(request, proyecto)
# TODO Enviar "resguardo" al solicitante. PDF?
proyecto.estado = "SOLICITADO"
......@@ -500,8 +526,9 @@ class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView):
},
)
def _enviar_solicitudes_visto_bueno(self, request, proyecto):
def _enviar_solicitudes_visto_bueno_centro(self, request, proyecto):
"""Envia un mensaje al responsable del centro solicitando su visto bueno."""
try:
validate_email(proyecto.centro.email_decano)
except ValidationError:
......@@ -515,7 +542,7 @@ class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView):
return
send_templated_mail(
template_name="solicitud_visto_bueno",
template_name="solicitud_visto_bueno_centro",
from_email=None, # settings.DEFAULT_FROM_EMAIL
recipient_list=[proyecto.centro.email_decano],
context={
......@@ -532,6 +559,40 @@ class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView):
},
)
def _is_email_valid(self, email):
"""Validate email address"""
try:
validate_email(email)
except ValidationError:
return False
return True
def _enviar_solicitudes_visto_bueno_estudio(self, request, proyecto):
"""Envia mensaje a los coordinadores del plan solicitando su visto bueno."""
email_coordinadores_estudio = [
f"{p.email_coordinador}"
for p in proyecto.estudio.planes.all()
if self._is_email_valid(p.email_coordinador)
]
send_templated_mail(
template_name="solicitud_visto_bueno_estudio",
from_email=None, # settings.DEFAULT_FROM_EMAIL
recipient_list=email_coordinadores_estudio,
context={
"nombre_coordinador": request.user.get_full_name(),
"titulo_proyecto": proyecto.titulo,
"programa_proyecto": f"{proyecto.programa.nombre_corto} "
f"({proyecto.programa.nombre_largo})",
"descripcion_proyecto": pypandoc.convert_text(
proyecto.descripcion, "md", format="html"
).replace("\\\n", "\n"),
"site_url": settings.SITE_URL,
},
)
def test_func(self):
# TODO: Comprobar fecha
return self.es_coordinador(self.kwargs["pk"])
......@@ -557,7 +618,14 @@ class ProyectoUpdateFieldView(LoginRequiredMixin, ChecksMixin, UpdateView):
):
raise Http404(_("No puede editar ese campo."))
if campo not in ("titulo", "departamento", "licencia", "ayuda", "visto_bueno"):
if campo not in (
"titulo",
"departamento",
"licencia",
"ayuda",
"visto_bueno_centro",
"visto_bueno_estudio",
):
formulario = modelform_factory(
Proyecto, fields=(campo,), widgets={campo: SummernoteWidget()}
)
......@@ -585,9 +653,18 @@ class ProyectoUpdateFieldView(LoginRequiredMixin, ChecksMixin, UpdateView):
return super().get_form_class()
def test_func(self):
return self.es_coordinador(self.kwargs["pk"]) or (
self.kwargs["campo"] == "visto_bueno"
and self.es_decano_o_director(self.kwargs["pk"])
"""Devuelve si el usuario está autorizado a modificar este campo."""
return (
self.es_coordinador(self.kwargs["pk"])
or (
self.kwargs["campo"] == "visto_bueno_centro"
and self.es_decano_o_director(self.kwargs["pk"])
)
or (
self.kwargs["campo"] == "visto_bueno_estudio"
and self.es_coordinador_estudio(self.kwargs["pk"])
)
)
......@@ -633,16 +710,25 @@ class ProyectosUsuarioView(LoginRequiredMixin, TemplateView):
)
try:
nip_decano = int(usuario.username)
nip_usuario = int(usuario.username)
except ValueError:
nip_decano = 0
nip_usuario = 0
centros_dirigidos = Centro.objects.filter(nip_decano=nip_decano).all()
centros_dirigidos = Centro.objects.filter(nip_decano=nip_usuario).all()
if centros_dirigidos:
context["proyectos_centros_dirigidos"] = Proyecto.objects.filter(
convocatoria_id=anyo,
programa__requiere_visto_bueno=True,
programa__requiere_visto_bueno_centro=True,
centro__in=centros_dirigidos,
).all()
planes_coordinados = Plan.objects.filter(nip_coordinador=nip_usuario).all()
if planes_coordinados:
id_estudios_coordinados = set([p.estudio_id for p in planes_coordinados])
context["proyectos_estudios_coordinados"] = Proyecto.objects.filter(
convocatoria_id=anyo,
programa__requiere_visto_bueno_estudio=True,
estudio_id__in=id_estudios_coordinados,
)
return context
......@@ -29,46 +29,61 @@
<th scope="row"><strong>{% trans 'Programa' %}</strong>:</th>
<td>{{ proyecto.programa.nombre_corto }} ({{ proyecto.programa.nombre_largo }})</td>
</tr>
{% if proyecto.linea %}
<tr>
<th scope="row"><strong>{% trans 'Línea' %}</strong>:</th>
<td>{{ proyecto.linea.nombre }}</td>
</tr>
{% endif %}
{% if proyecto.centro %}
<tr>
<th scope="row"><strong>{% trans 'Centro' %}</strong>:</th>
<td>{{ proyecto.centro.nombre }}</td>
</tr>
{% endif %}
{% if proyecto.estudio %}
<tr>
<th scope="row"><strong>{% trans 'Estudio' %}</strong>:</th>
<td>{{ proyecto.estudio.nombre }}</td>
</tr>
{% endif %}
{% if pp_coordinador %}
<tr>
<th scope="row"><strong>{{ pp_coordinador.get_cargo }}</strong>:</th>
<td>{{ pp_coordinador.usuario.get_full_name }}</td>
</tr>
{% endif %}
{% if pp_coordinador_2 %}
<tr>
<th scope="row"><strong>{{ pp_coordinador_2.get_cargo }}</strong>:</th>
<td>{{ pp_coordinador_2.usuario.get_full_name }}</td>
</tr>
{% endif %}
<tr>
<th scope="row"><strong>{% trans 'Estado' %}</strong>:</th>
<td>{{ proyecto.get_estado_display }}</td>
</tr>
{% if proyecto.programa.requiere_visto_bueno %}
{% if proyecto.programa.requiere_visto_bueno_centro %}
<tr>
<th scope="row"><strong>{% trans 'Visto bueno del centro' %}</strong>:</th>
<td>{{ proyecto.visto_bueno_centro | yesno:"Sí,No,—" }}</td>
</tr>
{% endif %}
{% if proyecto.programa.requiere_visto_bueno_estudio %}
<tr>
<th scope="row"><strong>{% trans 'Visto bueno' %}</strong>:</th>
<td>{{ proyecto.visto_bueno | yesno:"Sí,No,—" }}</td>
<th scope="row"><strong>{% trans 'Visto bueno del estudio' %}</strong>:</th>
<td>{{ proyecto.visto_bueno_estudio | yesno:"Sí,No,—" }}</td>
</tr>
{% endif %}
</table>
</div>
<br />
......
......@@ -162,10 +162,50 @@
<td><a href="{% url 'proyecto_detail' proyecto.id %}">{{ proyecto.titulo }}</a></td>
<td>{{proyecto.get_usuario_coordinador.get_full_name}}</td>
<td>{{proyecto.get_estado_display}}</td>
<td>{{proyecto.visto_bueno | yesno:"Sí,No,—"}}</td>
<td>{{proyecto.visto_bueno_centro | yesno:"Sí,No,—"}}</td>
<td>
{% if proyecto.estado == 'SOLICITADO' %}
<a href="{% url 'proyecto_update_field' proyecto.id 'visto_bueno' %}" class="btn btn-info btn-sm">
<a href="{% url 'proyecto_update_field' proyecto.id 'visto_bueno_centro' %}" class="btn btn-info btn-sm">
<span class="fas fa-pencil-alt" aria-hidden="true"></span>&nbsp; {% trans 'Editar' %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
<br /><br />
{% if proyectos_estudios_coordinados %}
<h1 id="del-estudio">{% trans "Proyectos estratégicos de mis estudios" %}</h1>
<hr />
<br />
<div class="table-responsive">
<table class="table table-hover table-striped table-sm cabecera-azul" aria-describedby="del-estudio">
<thead>
<tr>
<th scope="col">{% trans 'Programa' %}</th>
<th scope="col">{% trans 'Línea' %}</th>
<th scope="col">{% trans 'Título' %}</th>
<th scope="col">{% trans 'Coordinador' %}</th>
<th scope="col">{% trans 'Estado' %}</th>
<th scope="col">{% trans 'Visto bueno' %}</th>
<th scope="col">{% trans 'Acción' %}</th>
</tr>
</thead>
{% for proyecto in proyectos_estudios_coordinados %}
<tr>
<td>{{proyecto.programa.nombre_corto}}</td>
<td>{{proyecto.linea.nombre}}</td>
<td><a href="{% url 'proyecto_detail' proyecto.id %}">{{ proyecto.titulo }}</a></td>
<td>{{proyecto.get_usuario_coordinador.get_full_name}}</td>
<td>{{proyecto.get_estado_display}}</td>
<td>{{proyecto.visto_bueno_estudio | yesno:"Sí,No,—"}}</td>
<td>
{% if proyecto.estado == 'SOLICITADO' %}
<a href="{% url 'proyecto_update_field' proyecto.id 'visto_bueno_estudio' %}" class="btn btn-info btn-sm">
<span class="fas fa-pencil-alt" aria-hidden="true"></span>&nbsp; {% trans 'Editar' %}
</a>
{% endif %}
......
{% block subject %}VºBº del proyecto «{{titulo_proyecto}}»{% endblock %}
{% block subject %}VºBº del proyecto «{{ titulo_proyecto }}»{% endblock %}
{% block plain %}
{% if tratamiento_decano in 'Decana,Directora' %}
Estimada {{nombre_decano}}:
Estimada {{ nombre_decano }}:
{% elif tratamiento_decano in 'Decano,Director' %}
Estimado {{nombre_decano}}:
Estimado {{ nombre_decano }}:
{% else %}
Estimad@ {{nombre_decano}}:
Estimad@ {{ nombre_decano }}:
{% endif %}
{{nombre_coordinador}} ha solicitado el siguiente Proyecto de Innovación Docente:
{{ nombre_coordinador }} ha solicitado el siguiente Proyecto de Innovación Docente:
Título: {{titulo_proyecto}}
Programa: {{programa_proyecto}}
Título: {{ titulo_proyecto }}
Programa: {{ programa_proyecto }}
Descripción: {{ descripcion_proyecto | striptags }}
Los proyectos de este programa deben ser impulsados por el centro.
Por ello, para que solicitud anterior pueda ser evaluada, debe contar con
el visto bueno del responsable del centro.
Por ello, para que la solicitud anterior pueda ser evaluada,
debe contar con el visto bueno del responsable del centro.
Para dar su visto bueno a dicha solicitud, visite la web <{{site_url}}>
Para dar su visto bueno a dicha solicitud, visite la web <{{ site_url }}>
e inicie sesión con su NIP y contraseña administrativa.
......
{% block subject %}VºBº del proyecto «{{ titulo_proyecto }}»{% endblock %}
{% block plain %}
{{ nombre_coordinador }} ha solicitado el siguiente Proyecto de Innovación Docente:
Título: {{ titulo_proyecto }}
Programa: {{ programa_proyecto }}
Descripción: {{ descripcion_proyecto | striptags }}
Para que puedan ser evaluadas, las solicitudes de proyecto de este programa
deben contar con la aprobación de un coordinador del estudio.
Para dar su visto bueno a dicha solicitud, visite la web <{{ site_url }}>
e inicie sesión con su NIP y contraseña administrativa.
Atentamente,
Vicerrectorado de Política Académica
{% endblock %}
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