views.py 16.3 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
        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
71 72 73 74 75
        pp = (
            proyecto.participantes.filter(usuario=usuario_actual)
            .exclude(tipo_participacion="invitacion_rehusada")
            .all()
        )
76 77 78
        self.permission_denied_message = _("Usted no está vinculado a este proyecto.")

        return True if pp else False
79 80

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


94 95 96 97 98 99
class AyudaView(TemplateView):
    template_name = "ayuda.html"


class HomePageView(TemplateView):
    template_name = "home.html"
100 101


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

    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
118
        kwargs.update(self.kwargs)  # self.kwargs contains all URL conf params
119
        kwargs["request"] = self.request
120 121 122
        return kwargs

    def get_success_url(self, **kwargs):
123 124 125
        return reverse_lazy(
            "proyecto_detail", kwargs={"pk": self.kwargs["proyecto_id"]}
        )
126

127 128 129
    def test_func(self):
        # TODO: Comprobar estado del proyecto, fecha.
        return self.es_coordinador(self.kwargs["proyecto_id"])
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 180 181 182 183
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)


184 185
class ParticipanteDeleteView(LoginRequiredMixin, ChecksMixin, DeleteView):
    """Borra un registro de ParticipanteProyecto"""
186 187 188 189 190 191 192

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

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

193 194
    def test_func(self):
        return self.es_coordinador(self.get_object().proyecto.id)
195

196

197 198
class ProyectoCreateView(LoginRequiredMixin, ChecksMixin, CreateView):
    """Crea una nueva solicitud de proyecto"""
199

200 201
    model = Proyecto
    template_name = "proyecto/new.html"
202 203
    # fields = ["titulo", "descripcion", "programa", "linea", "centro", "estudio"]
    form_class = ProyectoForm
204 205

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

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

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

246 247 248 249 250
    def test_func(self):
        # TODO: Comprobar usuario para Proyectos de titulación y POU.
        # TODO: Comprobar fecha
        return self.es_pas_o_pdi()

251

252
class ProyectoDetailView(LoginRequiredMixin, ChecksMixin, DetailView):
253 254
    """Muestra una solicitud de proyecto."""

255 256 257 258 259 260 261 262 263 264 265 266 267 268
    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

269 270 271 272 273 274 275 276
        participantes = (
            self.object.participantes.filter(tipo_participacion="participante")
            .order_by("usuario__first_name", "usuario__last_name")
            .all()
        )
        context["participantes"] = participantes

        invitados = (
277 278 279 280
            self.object.participantes.filter(
                tipo_participacion__in=["invitado", "invitacion_rehusada"]
            )
            .order_by("tipo_participacion", "usuario__first_name", "usuario__last_name")
281 282 283 284
            .all()
        )
        context["invitados"] = invitados

285 286
        context["campos"] = json.loads(self.object.programa.campos)

287
        return context
288

289 290
    def test_func(self):
        # TODO: Los evaluadores y gestores también tendrán que tener acceso.
291
        return self.esta_vinculado(self.kwargs["pk"])
292 293 294 295


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

297
    El proyecto pasa de estado «Borrador» a estado «Solicitado».
298
    Se envían correos a los agentes involucrados.
299
    """
300

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

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

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

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

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

379 380 381
    def test_func(self):
        # TODO: Comprobar fecha
        return self.es_coordinador(self.kwargs["pk"])
382

383 384

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

387 388 389 390 391 392
    # TODO: Comprobar estado/fecha
    model = Proyecto
    template_name = "proyecto/update.html"

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

        if campo not in ("titulo", "departamento", "licencia", "ayuda"):
405 406 407 408 409
            return modelform_factory(
                Proyecto, fields=(campo,), widgets={campo: SummernoteWidget()}
            )
        self.fields = (campo,)
        return super().get_form_class()
410

411 412 413
    def test_func(self):
        return self.es_coordinador(self.kwargs["pk"])

414

415 416
class ProyectosUsuarioView(LoginRequiredMixin, TemplateView):
    """Lista los proyectos a los que está vinculado el usuario actual."""
417

418 419 420
    template_name = "proyecto/mis-proyectos.html"

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

        return context