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
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."""
33
        proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
34 35 36 37 38 39 40 41 42 43 44 45 46
        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."""
47
        proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
48
        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

    def es_invitado(self, proyecto_id):
        """Devuelve si el usuario actual es invitado del proyecto indicado."""
58
        proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
59
        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
        return True if pp else False

    def esta_vinculado(self, proyecto_id):
        """Devuelve si el usuario actual está vinculado al proyecto indicado."""
69
        proyecto = get_object_or_404(Proyecto, pk=proyecto_id)
70
        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
    form_class = ProyectoForm
203 204

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

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

221
    def _guardar_coordinador(self, proyecto):
222 223
        # Los PIET debe solicitarlos uno de los coordinadores del estudio
        # ("coordinador principal") quien podrá nombrar a otro coordinador.
224 225 226 227 228
        if proyecto.programa.nombre_corto == "PIET":
            tipo_participacion = "coordinador_principal"
        else:
            tipo_participacion = "coordinador"

229
        pp = ParticipanteProyecto(
230 231 232 233
            proyecto=proyecto,
            tipo_participacion=TipoParticipacion(nombre=tipo_participacion),
            usuario=self.request.user,
        )
234
        pp.save()
235 236 237 238 239 240 241 242 243 244

    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()

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

250

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

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

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

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

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

286 287 288 289
        context["permitir_edicion"] = (
            self.es_coordinador(self.object.id) and self.object.en_borrador()
        )

290
        return context
291

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


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

300
    El proyecto pasa de estado «Borrador» a estado «Solicitado».
301
    Se envían correos a los agentes involucrados.
302
    """
303

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

316
        if proyecto.ayuda > proyecto.programa.max_ayuda:
317 318 319 320
            messages.error(
                request,
                _(
                    f"La ayuda solicitada ({proyecto.ayuda} €) excede el máximo "
321
                    f"permitido para este programa ({proyecto.programa.max_ayuda} €)."
322 323 324 325 326 327 328 329 330
                ),
            )
            return super().post(request, *args, **kwargs)

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

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

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

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

382 383 384
    def test_func(self):
        # TODO: Comprobar fecha
        return self.es_coordinador(self.kwargs["pk"])
385

386 387

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

390 391 392 393 394 395
    # TODO: Comprobar estado/fecha
    model = Proyecto
    template_name = "proyecto/update.html"

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

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

414 415 416
    def test_func(self):
        return self.es_coordinador(self.kwargs["pk"])

417

418 419
class ProyectosUsuarioView(LoginRequiredMixin, TemplateView):
    """Lista los proyectos a los que está vinculado el usuario actual."""
420

421 422 423
    template_name = "proyecto/mis-proyectos.html"

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

        return context