views.py 14.4 KB
Newer Older
1
import json
2
from datetime import date
3
from django.conf import settings
4
from django.contrib import messages
5
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
6
from django.forms.models import modelform_factory
7 8 9
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse_lazy
10
from django.utils.translation import gettext_lazy as _
11
from django.views.generic import DetailView, RedirectView, TemplateView
12
from django.views.generic.edit import CreateView, UpdateView, DeleteView
13
from django_summernote.widgets import SummernoteWidget
14

15 16
from templated_email import send_templated_mail

17
from .forms import InvitacionForm, ProyectoForm
18 19 20 21 22 23 24 25 26
from .models import (
    Convocatoria,
    Evento,
    ParticipanteProyecto,
    Proyecto,
    Registro,
    TipoParticipacion,
)

27

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
class ChecksMixin(UserPassesTestMixin):
    """Proporciona comprobaciones para autorizar o no una acción a un usuario."""

    def es_coordinador(self, proyecto_id):
        """Devuelve si el usuario actual es coordinador del proyecto indicado."""
        proyecto = Proyecto.objects.get(id=proyecto_id)
        usuario_actual = self.request.user
        coordinadores_participantes = proyecto.participantes.filter(
            tipo_participacion__in=["coordinador", "coordinador_principal"]
        ).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."""
        proyecto = Proyecto.objects.get(id=proyecto_id)
        usuario_actual = self.request.user
49 50 51
        pp = proyecto.participantes.filter(
            usuario=usuario_actual, tipo_participacion="participante"
        ).all()
52 53
        self.permission_denied_message = _("Usted no es participante de este proyecto.")

54
        return True if pp else False
55 56 57 58 59

    def es_invitado(self, proyecto_id):
        """Devuelve si el usuario actual es invitado del proyecto indicado."""
        proyecto = Proyecto.objects.get(id=proyecto_id)
        usuario_actual = self.request.user
60 61 62
        pp = proyecto.participantes.filter(
            usuario=usuario_actual, tipo_participacion="invitado"
        ).all()
63 64
        self.permission_denied_message = _("Usted no está invitado a este proyecto.")

65 66 67 68 69 70 71 72 73 74
        return True if pp else False

    def esta_vinculado(self, proyecto_id):
        """Devuelve si el usuario actual está vinculado al proyecto indicado."""
        proyecto = Proyecto.objects.get(id=proyecto_id)
        usuario_actual = self.request.user
        pp = proyecto.participantes.filter(usuario=usuario_actual).all()
        self.permission_denied_message = _("Usted no está vinculado a este proyecto.")

        return True if pp else False
75 76

    def es_pas_o_pdi(self):
77 78 79
        """
        Devuelve si el usuario actual es PAS o PDI de la UZ o de sus centros adscritos.
        """
80 81 82 83 84 85
        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
86
            for col_autorizado in ["PAS", "ADS", "PDI"]
87 88 89
        )


90 91 92 93 94 95
class AyudaView(TemplateView):
    template_name = "ayuda.html"


class HomePageView(TemplateView):
    template_name = "home.html"
96 97


98 99
class InvitacionView(LoginRequiredMixin, ChecksMixin, CreateView):
    """Muestra un formulario para invitar a una persona a un proyecto determinado."""
100 101 102 103 104 105 106 107 108 109 110 111 112 113

    form_class = InvitacionForm
    model = ParticipanteProyecto
    template_name = "participante-proyecto/invitar.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        proyecto_id = self.kwargs["proyecto_id"]
        context["proyecto"] = Proyecto.objects.get(id=proyecto_id)
        return context

    def get_form_kwargs(self, **kwargs):
        kwargs = super().get_form_kwargs()
        # Update the kwargs for the form init method with ours
114
        kwargs.update(self.kwargs)  # self.kwargs contains all URL conf params
115
        kwargs["request"] = self.request
116 117 118
        return kwargs

    def get_success_url(self, **kwargs):
119 120 121
        return reverse_lazy(
            "proyecto_detail", kwargs={"pk": self.kwargs["proyecto_id"]}
        )
122

123 124 125
    def test_func(self):
        # TODO: Comprobar estado del proyecto, fecha.
        return self.es_coordinador(self.kwargs["proyecto_id"])
126

127 128 129

class ParticipanteDeleteView(LoginRequiredMixin, ChecksMixin, DeleteView):
    """Borra un registro de ParticipanteProyecto"""
130 131 132 133 134 135 136

    model = ParticipanteProyecto
    template_name = "participante-proyecto/confirm_delete.html"

    def get_success_url(self):
        return reverse_lazy("proyecto_detail", args=[self.object.proyecto.id])

137 138
    def test_func(self):
        return self.es_coordinador(self.get_object().proyecto.id)
139

140

141 142
class ProyectoCreateView(LoginRequiredMixin, ChecksMixin, CreateView):
    """Crea una nueva solicitud de proyecto"""
143

144 145
    model = Proyecto
    template_name = "proyecto/new.html"
146 147
    # fields = ["titulo", "descripcion", "programa", "linea", "centro", "estudio"]
    form_class = ProyectoForm
148 149

    def form_valid(self, form):
150 151
        # This method is called when valid form data has been POSTed,
        # to do custom logic on form data. It should return an HttpResponse.
152 153 154 155 156
        proyecto = form.save()
        self._guardar_coordinador(proyecto)
        self._registrar_creacion(proyecto)
        return redirect("proyecto_detail", proyecto.id)

157
    def get_form(self, form_class=None):
158 159 160 161
        """
        Devuelve el formulario añadiendo automáticamente el campo Convocatoria,
        que es requerido.
        """
162 163 164 165
        form = super(ProyectoCreateView, self).get_form(form_class)
        form.instance.convocatoria = Convocatoria(date.today().year)
        return form

166
    def _guardar_coordinador(self, proyecto):
167 168
        # Los PIET debe solicitarlos uno de los coordinadores del estudio
        # ("coordinador principal") quien podrá nombrar a otro coordinador.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
        if proyecto.programa.nombre_corto == "PIET":
            tipo_participacion = "coordinador_principal"
        else:
            tipo_participacion = "coordinador"

        participanteProyecto = ParticipanteProyecto(
            proyecto=proyecto,
            tipo_participacion=TipoParticipacion(nombre=tipo_participacion),
            usuario=self.request.user,
        )
        participanteProyecto.save()

    def _registrar_creacion(self, proyecto):
        evento = Evento.objects.get(nombre="creacion_solicitud")
        registro = Registro(
            descripcion="Creación inicial de la solicitud",
            evento=evento,
            proyecto=proyecto,
        )
        registro.save()

190 191 192 193 194
    def test_func(self):
        # TODO: Comprobar usuario para Proyectos de titulación y POU.
        # TODO: Comprobar fecha
        return self.es_pas_o_pdi()

195

196
class ProyectoDetailView(LoginRequiredMixin, ChecksMixin, DetailView):
197 198
    """Muestra una solicitud de proyecto."""

199 200 201 202 203 204 205 206 207 208 209 210 211 212
    model = Proyecto
    template_name = "proyecto/detail.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        coordinador_principal = self.object.get_participante_or_none(
            "coordinador_principal"
        )
        context["coordinador_principal"] = coordinador_principal

        coordinador = self.object.get_participante_or_none("coordinador")
        context["coordinador"] = coordinador

213 214 215 216 217 218 219 220 221 222 223 224 225 226
        participantes = (
            self.object.participantes.filter(tipo_participacion="participante")
            .order_by("usuario__first_name", "usuario__last_name")
            .all()
        )
        context["participantes"] = participantes

        invitados = (
            self.object.participantes.filter(tipo_participacion="invitado")
            .order_by("usuario__first_name", "usuario__last_name")
            .all()
        )
        context["invitados"] = invitados

227 228
        context["campos"] = json.loads(self.object.programa.campos)

229
        return context
230

231 232
    def test_func(self):
        # TODO: Los evaluadores y gestores también tendrán que tener acceso.
233
        return self.esta_vinculado(self.kwargs["pk"])
234 235 236 237


class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView):
    """Presenta una solicitud de proyecto.
238

239
    El proyecto pasa de estado «Borrador» a estado «Solicitado».
240
    Se envían correos a los agentes involucrados.
241
    """
242

243 244 245 246 247 248 249 250
    def get_redirect_url(self, *args, **kwargs):
        return reverse_lazy("proyecto_detail", args=[kwargs.get("pk")])

    def post(self, request, *args, **kwargs):
        proyecto_id = kwargs.get("pk")
        proyecto = Proyecto.objects.get(pk=proyecto_id)

        # TODO ¿Chequear el estado actual del proyecto?
251 252 253 254
        if not proyecto.ayuda:
            messages.error(request, _("No ha indicado la ayuda solicitada."))
            return super().post(request, *args, **kwargs)

255
        if proyecto.ayuda > proyecto.programa.max_ayuda:
256 257 258 259 260 261 262 263 264 265 266 267 268 269
            messages.error(
                request,
                _(
                    f"La ayuda solicitada ({proyecto.ayuda} €) excede el máximo "
                    "permitido para este programa ({proyecto.programa.max_ayuda} €)."
                ),
            )
            return super().post(request, *args, **kwargs)

        if not proyecto.tiene_invitados():
            messages.error(
                request,
                _("La solicitud debe incluir al menos un invitado a participar."),
            )
270
            return super().post(request, *args, **kwargs)
271

272 273 274
        self._enviar_invitaciones(request, proyecto)
        if proyecto.programa.nombre_corto in ["PIEC", "PRACUZ"]:
            self._enviar_solicitudes_visto_bueno(request, proyecto)
275
        # TODO Enviar "resguardo" al solicitante. PDF?
276 277 278 279 280 281 282 283 284

        proyecto.estado = "SOLICITADO"
        proyecto.save()

        # TODO Modificar detail.html para no mostrar botones de edición/presentación
        messages.success(request, _("Su solicitud de proyecto ha sido presentada."))
        return super().post(request, *args, **kwargs)

    def _enviar_invitaciones(self, request, proyecto):
285
        """Envia un mensaje a cada uno de los invitados al proyecto."""
286 287 288 289 290 291 292 293 294 295
        for invitado in proyecto.participantes.filter(tipo_participacion="invitado"):
            send_templated_mail(
                template_name="invitacion",
                from_email=None,  # settings.DEFAULT_FROM_EMAIL
                recipient_list=[invitado.usuario.email],
                context={
                    "nombre_coordinador": request.user.get_full_name(),
                    "nombre_invitado": invitado.usuario.get_full_name(),
                    "sexo_invitado": invitado.usuario.sexo,
                    "titulo_proyecto": proyecto.titulo,
296 297
                    "programa_proyecto": f"{proyecto.programa.nombre_corto} "
                    + f"({proyecto.programa.nombre_largo})",
298
                    "descripcion_proyecto": proyecto.descripcion,
299
                    "site_url": settings.SITE_URL,
300 301 302
                },
            )

303
    def _enviar_solicitudes_visto_bueno(self, request, proyecto):
304
        """Envia un mensaje al responsable del centro solicitando su visto bueno."""
305 306 307 308 309 310 311 312 313
        send_templated_mail(
            template_name="solicitud_visto_bueno",
            from_email=None,  # settings.DEFAULT_FROM_EMAIL
            recipient_list=[proyecto.centro.email_decano],
            context={
                "nombre_coordinador": request.user.get_full_name(),
                "nombre_decano": proyecto.centro.nombre_decano,
                "tratamiento_decano": proyecto.centro.tratamiento_decano,
                "titulo_proyecto": proyecto.titulo,
314 315
                "programa_proyecto": f"{proyecto.programa.nombre_corto} "
                f"({proyecto.programa.nombre_largo})",
316 317 318 319
                "descripcion_proyecto": proyecto.descripcion,
                "site_url": settings.SITE_URL,
            },
        )
320

321 322 323
    def test_func(self):
        # TODO: Comprobar fecha
        return self.es_coordinador(self.kwargs["pk"])
324

325 326

class ProyectoUpdateFieldView(LoginRequiredMixin, ChecksMixin, UpdateView):
327
    """Actualiza un campo de una solicitud de proyecto."""
328

329 330 331 332 333 334
    # TODO: Comprobar estado/fecha
    model = Proyecto
    template_name = "proyecto/update.html"

    def get_form_class(self, **kwargs):
        campo = self.kwargs["campo"]
335
        if campo in (
336
            "centro",
337
            "codigo",
338
            "convocatoria",
339 340
            "estado",
            "estudio",
341 342 343
            "linea",
            "programa",
        ):
344 345 346
            raise Http404(_("No puede editar ese campo."))

        if campo not in ("titulo", "departamento", "licencia", "ayuda"):
347 348 349 350 351
            return modelform_factory(
                Proyecto, fields=(campo,), widgets={campo: SummernoteWidget()}
            )
        self.fields = (campo,)
        return super().get_form_class()
352

353 354 355
    def test_func(self):
        return self.es_coordinador(self.kwargs["pk"])

356

357 358
class ProyectosUsuarioView(LoginRequiredMixin, TemplateView):
    """Lista los proyectos a los que está vinculado el usuario actual."""
359

360 361 362
    template_name = "proyecto/mis-proyectos.html"

    def get_context_data(self, **kwargs):
363
        usuario = self.request.user
364
        context = super().get_context_data(**kwargs)
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
        context["proyectos_coordinados"] = (
            Proyecto.objects.filter(
                participantes__usuario=usuario,
                participantes__tipo_participacion_id__in=[
                    "coordinador",
                    "coordinador_principal",
                ],
            )
            .order_by("programa__nombre_corto", "linea__nombre", "titulo")
            .all()
        )
        context["proyectos_participados"] = (
            Proyecto.objects.filter(
                participantes__usuario=usuario,
                participantes__tipo_participacion_id="participante",
            )
            .order_by("programa__nombre_corto", "linea__nombre", "titulo")
            .all()
        )
        context["proyectos_invitado"] = (
            Proyecto.objects.filter(
                participantes__usuario=usuario,
                participantes__tipo_participacion_id="invitado",
            )
            .order_by("programa__nombre_corto", "linea__nombre", "titulo")
            .all()
        )
392 393

        return context