feat Listar todos los proyectos presentados.

Crear grupo Gestores, y darle permiso para esta acción.
parent f88e60ba
Pipeline #515 failed with stage
in 0 seconds
......@@ -17,6 +17,7 @@ cx-oracle = "*"
django-annoying = "*"
django-crispy-forms = "==1.8.1"
django-summernote = "*"
django-tables2 = "==2.2.1"
django-templated-email = "*"
pypandoc = "*"
......
This diff is collapsed.
# Generated by Django 3.0.2 on 2020-01-31 11:44
from django.apps import apps as django_apps
from django.db import migrations, models
import django.db.models.deletion
def geo_post_migrate_signal(apps, schema_editor):
"""Emit the post-migrate signal during the migration.
Permissions are not actually created during or after an individual migration,
but are triggered by a post-migrate signal which is sent after the
`python manage.py migrate` command completes successfully.
This is necessary because this permission is used in the next migration.
"""
indo_config = django_apps.get_app_config("indo")
models.signals.post_migrate.send(
sender=indo_config,
app_config=indo_config,
verbosity=2,
interactive=False,
using=schema_editor.connection.alias,
)
class Migration(migrations.Migration):
dependencies = [("indo", "0001_initial")]
operations = [
migrations.AlterModelOptions(
name="proyecto",
options={
"permissions": [
("listar_proyectos", "Puede ver el listado de todos los proyectos.")
]
},
),
migrations.AlterField(
model_name="plan",
name="estudio",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="planes",
to="indo.Estudio",
),
),
migrations.AlterField(
model_name="proyecto",
name="descripcion",
field=models.TextField(
help_text=(
"Resumen sucinto del proyecto. "
"Máximo recomendable: un párrafo de 10 líneas."
),
max_length=4095,
null=True,
verbose_name="Resumen",
),
),
migrations.RunPython(geo_post_migrate_signal),
]
# Generated by Django 3.0.2 on 2020-01-31 11:45
# https://docs.djangoproject.com/en/3.0/howto/writing-migrations/
from django.db import migrations
def add_managers_group(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")
group, created = Group.objects.get_or_create(name="Gestores")
if created:
print("Creado el grupo «Gestores».")
listar_proyectos = Permission.objects.get(codename="listar_proyectos")
group.permissions.add(listar_proyectos)
class Migration(migrations.Migration):
dependencies = [("indo", "0002_auto_20200131_1244")]
operations = [migrations.RunPython(add_managers_group)]
......@@ -55,13 +55,13 @@ class Centro(models.Model):
)
esta_activo = models.BooleanField(_("¿Activo?"), default=False)
def __str__(self):
return f"{self.nombre} ({self.academico_id_nk} / {self.rrhh_id_nk})"
class Meta:
unique_together = ["academico_id_nk", "rrhh_id_nk"]
ordering = ["nombre"]
def __str__(self):
return f"{self.nombre} ({self.academico_id_nk} / {self.rrhh_id_nk})"
class Convocatoria(models.Model):
id = models.PositiveSmallIntegerField(_("año"), primary_key=True)
......@@ -97,12 +97,12 @@ class Departamento(models.Model):
_("unidad de gasto"), blank=True, max_length=3, null=True
)
def __str__(self):
return f"{self.nombre} ({self.academico_id_nk} / {self.rrhh_id_nk})"
class Meta:
unique_together = ["academico_id_nk", "rrhh_id_nk"]
def __str__(self):
return f"{self.nombre} ({self.academico_id_nk} / {self.rrhh_id_nk})"
class Estudio(models.Model):
OPCIONES_RAMA = (
......@@ -120,12 +120,12 @@ class Estudio(models.Model):
rama = models.CharField(max_length=1, choices=OPCIONES_RAMA)
tipo_estudio = models.ForeignKey("TipoEstudio", on_delete=models.PROTECT)
def __str__(self):
return f"{self.nombre} ({self.tipo_estudio.nombre})"
class Meta:
ordering = ["nombre"]
def __str__(self):
return f"{self.nombre} ({self.tipo_estudio.nombre})"
class Evento(models.Model):
nombre = models.CharField(primary_key=True, max_length=31)
......@@ -472,12 +472,20 @@ class Proyecto(models.Model):
programa = models.ForeignKey("Programa", on_delete=models.PROTECT)
visto_bueno = models.BooleanField(_("Visto bueno"), null=True)
def en_borrador(self):
return self.estado == "BORRADOR"
class Meta:
permissions = [
("listar_proyectos", _("Puede ver el listado de todos los proyectos."))
]
def __str__(self):
return self.codigo
def get_absolute_url(self):
return reverse("proyecto_detail", args=[str(self.id)])
def en_borrador(self):
return self.estado == "BORRADOR"
def get_participante_or_none(self, tipo):
try:
return ParticipanteProyecto.objects.get(
......@@ -513,9 +521,6 @@ class Proyecto(models.Model):
num_invitados = self.participantes.filter(tipo_participacion="invitado").count()
return num_invitados >= 1
def __str__(self):
return self.codigo
class Registro(models.Model):
fecha = models.DateTimeField(auto_now_add=True)
......
import django_tables2 as tables
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from .models import Proyecto
class ProyectosTable(tables.Table):
def render_titulo(self, record):
enlace = reverse("proyecto_detail", args=[record.id])
return mark_safe(f"<a href='{enlace}'>{record.titulo}</a>")
coordinadores = tables.Column(
empty_values=(), orderable=False, verbose_name=_("Coordinador(es)")
)
def render_coordinadores(self, record):
coordinadores = record.get_coordinadores()
enlaces = [
f"<a href='mailto:{c.email}'>{c.get_full_name()}</a>" for c in coordinadores
]
return mark_safe(", ".join(enlaces))
class Meta:
attrs = {"class": "table table-striped table-hover cabecera-azul"}
model = Proyecto
fields = ("programa", "linea", "titulo", "coordinadores", "estado")
empty_text = _(
"Por el momento no se ha presentado ninguna solicitud de proyecto."
)
template_name = "django_tables2/bootstrap4.html"
per_page = 20
......@@ -13,6 +13,7 @@ from .views import (
ProyectoAnularView,
ProyectoCreateView,
ProyectoDetailView,
ProyectoListView,
ProyectoPresentarView,
ProyectoUpdateFieldView,
ProyectosUsuarioView,
......@@ -23,6 +24,11 @@ urlpatterns = [
path("", HomePageView.as_view(), name="home"),
path("summernote/", include("django_summernote.urls")),
path("ayuda/", AyudaView.as_view(), name="ayuda"),
path(
"gestion/proyecto/list/<int:anyo>",
ProyectoListView.as_view(),
name="proyecto_list",
),
path(
"participante-proyecto/aceptar_invitacion/<int:proyecto_id>",
ParticipanteAceptarView.as_view(),
......
......@@ -4,7 +4,11 @@ from datetime import date
import pypandoc
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.mixins import (
LoginRequiredMixin,
PermissionRequiredMixin,
UserPassesTestMixin,
)
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.forms.models import modelform_factory
......@@ -15,6 +19,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, RedirectView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django_summernote.widgets import SummernoteWidget
from django_tables2.views import SingleTableView
from templated_email import send_templated_mail
from .forms import InvitacionForm, ProyectoForm
......@@ -27,6 +32,7 @@ from .models import (
Registro,
TipoParticipacion,
)
from .tables import ProyectosTable
class ChecksMixin(UserPassesTestMixin):
......@@ -381,6 +387,27 @@ class ProyectoDetailView(LoginRequiredMixin, ChecksMixin, DetailView):
return self.esta_vinculado_o_es_decano(proyecto_id)
class ProyectoListView(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView):
"""Muestra una tabla de todos los proyectos presentados en una convocatoria."""
permission_required = "indo.listar_proyectos"
permission_denied_message = _("Sólo los gestores pueden acceder a esta página.")
table_class = ProyectosTable
template_name = "gestion/proyecto/tabla.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["anyo"] = self.kwargs["anyo"]
return context
def get_queryset(self):
return (
Proyecto.objects.filter(convocatoria__id=self.kwargs["anyo"])
.exclude(estado__in=["BORRADOR", "ANULADO"])
.order_by("programa__nombre_corto", "linea__nombre", "titulo")
)
class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView):
"""Presenta una solicitud de proyecto.
......
......@@ -60,6 +60,7 @@ INSTALLED_APPS = [
# 3rd Party
"crispy_forms", # https://github.com/django-crispy-forms/django-crispy-forms
"django_summernote", # https://github.com/summernote/django-summernote
"django_tables2", # https://github.com/jieter/django-tables2
"social_django", # https://github.com/python-social-auth/social-app-django
]
......
......@@ -8,11 +8,12 @@ django-annoying==0.10.6
django-crispy-forms==1.8.1
django-render-block==0.6
django-summernote==0.8.11.6
django-tables2==2.2.1
django-templated-email==2.3.0
django==3.0.2
idna==2.8
isodate==0.6.0
lxml==4.4.2
lxml==4.5.0
mysqlclient==1.4.6
oauthlib==3.1.0
pkgconfig==1.5.1
......@@ -23,10 +24,10 @@ python3-saml==1.9.0
pytz==2019.3
requests-oauthlib==1.3.0
requests==2.22.0
six==1.13.0
six==1.14.0
social-auth-app-django==3.1.0
social-auth-core[saml]==3.2.0
sqlparse==0.3.0
urllib3==1.25.7
wheel==0.33.6
urllib3==1.25.8
wheel==0.34.1
xmlsec==1.3.3
......@@ -10,6 +10,10 @@ body {
margin-top: 56px;
}
h1 > small {
color: gray;
}
/*** Navbar ***/
.bg-azul {
background-color: #213c71;
......
......@@ -8,10 +8,10 @@
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous"
/>
>
{# SubResource Integrity: openssl dgst -sha384 -binary FICHERO | openssl base64 #}
<link
rel="stylesheet"
......@@ -78,8 +78,29 @@
<ul class="navbar-nav ml-auto">
{% if user.is_authenticated %}
{% now "Y" as anyo_actual %}
{# if user|has_group:"Gestores" #}
<li class="nav-item dropdown">
<a
class="nav-link dropdown-toggle"
href=""
id="gestionMenu"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span class="fas fa-cog"></span>&nbsp; {% trans "Gestión" %}
</a>
<div class="dropdown-menu" aria-labelledby="gestionMenu">
<a class="dropdown-item" href="{% url 'proyecto_list' anyo_actual %}">
<span class="fas fa-th-list"></span>&nbsp; {% trans "Proyectos presentados" %}
</a>
</div>
</li>
{# endif #}
<li class="nav-item">
{% now "Y" as anyo_actual %}
<a class="nav-link" href="{% url 'mis_proyectos' anyo_actual %}">
<span class="fas fa-project-diagram"></span> &nbsp;{% trans "Mis proyectos" %}
</a>
......@@ -90,8 +111,9 @@
<span class="fas fa-question-circle"></span>&nbsp; {% trans "Ayuda" %}
</a>
</li>
<li class="nav-item">
{% if user.is_authenticated %}
{% if user.is_authenticated %}
<li class="nav-item dropdown">
<a
class="nav-link dropdown-toggle"
href="#"
......@@ -114,15 +136,18 @@
<span class="fas fa-sign-out-alt"></span> {% trans "Cerrar sesión" %}
</a>
</div>
{% else %}
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% lord_url %}">
<span class="fas fa-sign-in-alt"></span>&nbsp; {% trans "Iniciar sesión" %}
</a>
{% endif %}
</li>
</li>
{% endif %}
</ul>
</div>
</nav>
<div class="container">
{% include 'partials/messages.html' %}
{% block content %} {% endblock content %}
......@@ -173,18 +198,18 @@
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"
></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"
></script>
{% block extrajs %}{% endblock extrajs %}
......
{% extends 'base.html' %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title %}{% trans "Proyectos presentados" %}{% endblock title %}
{% block content %}
<div class='container-blanco'>
<h1 id="presentados">{% trans "Proyectos presentados" %} <small>{{ anyo }}</small></h1>
<hr />
<br />
<div class="alert-info alert fade-in">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<span class="fas fa-info-circle"></span>
{% trans "Aquí puede ver todos los proyectos presentados en esta convocatoria." %}
</div><br />
{% render_table table %}
</div>
{% endblock content %}
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