views.py 16.2 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
from django.http import Http404
8
from django.shortcuts import get_object_or_404, redirect
9
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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
class ParticipanteAceptarView(LoginRequiredMixin, RedirectView):
    """Aceptar la invitación a participar en un proyecto."""

    def get_redirect_url(self, *args, **kwargs):
        return reverse_lazy("proyectos_usuario_list")

    def post(self, request, *args, **kwargs):
        proyecto_id = kwargs.get("proyecto_id")
        proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
        usuario_actual = self.request.user
        pp = get_object_or_404(
            ParticipanteProyecto,
            proyecto_id=proyecto_id,
            usuario=usuario_actual,
            tipo_participacion="invitado",
        )
        pp.tipo_participacion_id = "participante"
        pp.save()

        messages.success(
            request,
            _(f"Ha pasado a ser participante del proyecto «{proyecto.titulo}»."),
        )
        return super().post(request, *args, **kwargs)


class ParticipanteDeclinarView(LoginRequiredMixin, RedirectView):
    """Declinar la invitación a participar en un proyecto."""

    def get_redirect_url(self, *args, **kwargs):
        return reverse_lazy("proyectos_usuario_list")

    def post(self, request, *args, **kwargs):
        proyecto_id = request.POST.get("proyecto_id")
        proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
        usuario_actual = self.request.user
        pp = get_object_or_404(
            ParticipanteProyecto,
            proyecto_id=proyecto_id,
            usuario=usuario_actual,
            tipo_participacion="invitado",
        )
        pp.tipo_participacion_id = "invitacion_rehusada"
        pp.save()

        messages.success(
            request,
            _(f"Ha rehusado ser participante del proyecto «{proyecto.titulo}»."),
        )
        return super().post(request, *args, **kwargs)


180 181
class ParticipanteDeleteView(LoginRequiredMixin, ChecksMixin, DeleteView):
    """Borra un registro de ParticipanteProyecto"""
182 183 184 185 186 187 188

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

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

189 190
    def test_func(self):
        return self.es_coordinador(self.get_object().proyecto.id)
191

192

193 194
class ProyectoCreateView(LoginRequiredMixin, ChecksMixin, CreateView):
    """Crea una nueva solicitud de proyecto"""
195

196 197
    model = Proyecto
    template_name = "proyecto/new.html"
198 199
    # fields = ["titulo", "descripcion", "programa", "linea", "centro", "estudio"]
    form_class = ProyectoForm
200 201

    def form_valid(self, form):
202 203
        # This method is called when valid form data has been POSTed,
        # to do custom logic on form data. It should return an HttpResponse.
204 205 206 207 208
        proyecto = form.save()
        self._guardar_coordinador(proyecto)
        self._registrar_creacion(proyecto)
        return redirect("proyecto_detail", proyecto.id)

209
    def get_form(self, form_class=None):
210 211 212 213
        """
        Devuelve el formulario añadiendo automáticamente el campo Convocatoria,
        que es requerido.
        """
214 215 216 217
        form = super(ProyectoCreateView, self).get_form(form_class)
        form.instance.convocatoria = Convocatoria(date.today().year)
        return form

218
    def _guardar_coordinador(self, proyecto):
219 220
        # Los PIET debe solicitarlos uno de los coordinadores del estudio
        # ("coordinador principal") quien podrá nombrar a otro coordinador.
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
        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()

242 243 244 245 246
    def test_func(self):
        # TODO: Comprobar usuario para Proyectos de titulación y POU.
        # TODO: Comprobar fecha
        return self.es_pas_o_pdi()

247

248
class ProyectoDetailView(LoginRequiredMixin, ChecksMixin, DetailView):
249 250
    """Muestra una solicitud de proyecto."""

251 252 253 254 255 256 257 258 259 260 261 262 263 264
    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

265 266 267 268 269 270 271 272
        participantes = (
            self.object.participantes.filter(tipo_participacion="participante")
            .order_by("usuario__first_name", "usuario__last_name")
            .all()
        )
        context["participantes"] = participantes

        invitados = (
273 274 275 276
            self.object.participantes.filter(
                tipo_participacion__in=["invitado", "invitacion_rehusada"]
            )
            .order_by("tipo_participacion", "usuario__first_name", "usuario__last_name")
277 278 279 280
            .all()
        )
        context["invitados"] = invitados

281 282
        context["campos"] = json.loads(self.object.programa.campos)

283
        return context
284

285 286
    def test_func(self):
        # TODO: Los evaluadores y gestores también tendrán que tener acceso.
287
        return self.esta_vinculado(self.kwargs["pk"])
288 289 290 291


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

293
    El proyecto pasa de estado «Borrador» a estado «Solicitado».
294
    Se envían correos a los agentes involucrados.
295
    """
296

297 298 299 300 301 302 303 304
    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?
305 306 307 308
        if not proyecto.ayuda:
            messages.error(request, _("No ha indicado la ayuda solicitada."))
            return super().post(request, *args, **kwargs)

309
        if proyecto.ayuda > proyecto.programa.max_ayuda:
310 311 312 313 314 315 316 317 318 319 320 321 322 323
            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."),
            )
324
            return super().post(request, *args, **kwargs)
325

326 327 328
        self._enviar_invitaciones(request, proyecto)
        if proyecto.programa.nombre_corto in ["PIEC", "PRACUZ"]:
            self._enviar_solicitudes_visto_bueno(request, proyecto)
329
        # TODO Enviar "resguardo" al solicitante. PDF?
330 331 332 333 334 335 336 337 338

        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):
339
        """Envia un mensaje a cada uno de los invitados al proyecto."""
340 341 342 343 344 345 346 347 348 349
        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,
350 351
                    "programa_proyecto": f"{proyecto.programa.nombre_corto} "
                    + f"({proyecto.programa.nombre_largo})",
352
                    "descripcion_proyecto": proyecto.descripcion,
353
                    "site_url": settings.SITE_URL,
354 355 356
                },
            )

357
    def _enviar_solicitudes_visto_bueno(self, request, proyecto):
358
        """Envia un mensaje al responsable del centro solicitando su visto bueno."""
359 360 361 362 363 364 365 366 367
        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,
368 369
                "programa_proyecto": f"{proyecto.programa.nombre_corto} "
                f"({proyecto.programa.nombre_largo})",
370 371 372 373
                "descripcion_proyecto": proyecto.descripcion,
                "site_url": settings.SITE_URL,
            },
        )
374

375 376 377
    def test_func(self):
        # TODO: Comprobar fecha
        return self.es_coordinador(self.kwargs["pk"])
378

379 380

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

383 384 385 386 387 388
    # TODO: Comprobar estado/fecha
    model = Proyecto
    template_name = "proyecto/update.html"

    def get_form_class(self, **kwargs):
        campo = self.kwargs["campo"]
389
        if campo in (
390
            "centro",
391
            "codigo",
392
            "convocatoria",
393 394
            "estado",
            "estudio",
395 396 397
            "linea",
            "programa",
        ):
398 399 400
            raise Http404(_("No puede editar ese campo."))

        if campo not in ("titulo", "departamento", "licencia", "ayuda"):
401 402 403 404 405
            return modelform_factory(
                Proyecto, fields=(campo,), widgets={campo: SummernoteWidget()}
            )
        self.fields = (campo,)
        return super().get_form_class()
406

407 408 409
    def test_func(self):
        return self.es_coordinador(self.kwargs["pk"])

410

411 412
class ProyectosUsuarioView(LoginRequiredMixin, TemplateView):
    """Lista los proyectos a los que está vinculado el usuario actual."""
413

414 415 416
    template_name = "proyecto/mis-proyectos.html"

    def get_context_data(self, **kwargs):
417
        usuario = self.request.user
418
        context = super().get_context_data(**kwargs)
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
        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()
        )
446 447

        return context