Editar cada campo de la memoria por separado, como texto enriquecido

En vez de editar la memoria como un gran formulario, con campos de
texto plano, hacerlo campo a campo, permitiendo tablas, imágenes, etc.
parent 98b0a93e
Pipeline #617 failed with stage
in 1 second
......@@ -2,6 +2,8 @@
from datetime import date
# Third-party
from django_summernote.fields import SummernoteTextField
from django_summernote.widgets import SummernoteWidget
from social_django.models import UserSocialAuth
from social_django.utils import load_strategy
......@@ -14,14 +16,22 @@ from django.utils.translation import gettext_lazy as _
# Local Django
from accounts.models import CustomUser
from accounts.pipeline import get_identidad
from .models import Linea, ParticipanteProyecto, Programa, Proyecto, TipoParticipacion
from .models import (
Linea,
MemoriaRespuesta,
ParticipanteProyecto,
Programa,
Proyecto,
TipoParticipacion,
)
class InvitacionForm(forms.ModelForm):
nip = forms.IntegerField(
label=_('NIP'),
help_text=_(
'Número de Identificación Personal en la Universidad de Zaragoza de la persona a invitar.'
'Número de Identificación Personal en la Universidad de Zaragoza'
' de la persona a invitar.'
),
)
......@@ -58,7 +68,7 @@ class InvitacionForm(forms.ModelForm):
# Comprobamos si el usuario ya existe en el sistema.
usuario = CustomUser.objects.get_or_none(username=nip)
# Si no existe previamente, lo creamos y actualizamos con los datos de Gestión de Identidades.
# Si no existe previamente, lo creamos y actualizamos con los datos de Identidades.
if not usuario:
usuario = self._crear_usuario(nip)
......@@ -66,7 +76,7 @@ class InvitacionForm(forms.ModelForm):
try:
get_identidad(load_strategy(self.request), None, usuario)
except Exception as ex:
# Si Gestión de Identidades devuelve un error, y finalizamos mostrando el mensaje de error.
# Si Identidades devuelve un error, finalizamos mostrando el mensaje de error.
raise forms.ValidationError('ERROR: ' + str(ex))
# Si el usuario no está activo, finalizamos explicando esta circunstancia.
......@@ -195,6 +205,25 @@ class EvaluadorForm(forms.ModelForm):
model = Proyecto
class MemoriaRespuestaForm(forms.ModelForm):
texto = SummernoteTextField()
class Meta:
fields = ('texto',)
model = MemoriaRespuesta
widgets = {'texto': SummernoteWidget()}
def as_p(self):
"Return this form rendered as HTML <p>s, without showing the label."
return self._html_output(
normal_row='<p%(html_class_attr)s><!-- %(label)s --> %(field)s%(help_text)s</p>',
error_row='%s',
row_ender='</p>',
help_text_html=' <span class="helptext">%s</span>',
errors_on_separate_row=True,
)
class ResolucionForm(forms.ModelForm):
class Meta:
fields = ('aceptacion_comision', 'ayuda_concedida', 'tipo_gasto', 'observaciones')
......
# Generated by Django 3.1.5 on 2021-02-02 13:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [('indo', '0014_auto_20201030_1510')]
operations = [
migrations.AlterUniqueTogether(name='centro', unique_together=set()),
migrations.AlterUniqueTogether(name='departamento', unique_together=set()),
migrations.AddConstraint(
model_name='centro',
constraint=models.UniqueConstraint(
fields=('academico_id_nk', 'rrhh_id_nk'), name='centro-unique-academico-rrhh'
),
),
migrations.AddConstraint(
model_name='departamento',
constraint=models.UniqueConstraint(
fields=('academico_id_nk', 'rrhh_id_nk'), name='departamento-unique-academico-rrhh'
),
),
migrations.AddConstraint(
model_name='memoriarespuesta',
constraint=models.UniqueConstraint(
fields=('proyecto_id', 'subapartado_id'), name='unique-proyecto-subapartado'
),
),
]
......@@ -41,7 +41,11 @@ class Centro(models.Model):
esta_activo = models.BooleanField(_('¿Activo?'), default=False)
class Meta:
unique_together = ['academico_id_nk', 'rrhh_id_nk']
constraints = [
models.UniqueConstraint(
fields=['academico_id_nk', 'rrhh_id_nk'], name="centro-unique-academico-rrhh"
)
]
ordering = ['nombre']
def __str__(self):
......@@ -132,7 +136,11 @@ class Departamento(models.Model):
unidad_gasto = models.CharField(_('unidad de gasto'), blank=True, max_length=3, null=True)
class Meta:
unique_together = ['academico_id_nk', 'rrhh_id_nk']
constraints = [
models.UniqueConstraint(
fields=['academico_id_nk', 'rrhh_id_nk'], name="departamento-unique-academico-rrhh"
)
]
def __str__(self):
return f'{self.nombre} ({self.academico_id_nk} / {self.rrhh_id_nk})'
......@@ -653,12 +661,30 @@ class MemoriaRespuesta(models.Model):
class Meta:
ordering = ('-proyecto__id', 'subapartado')
constraints = [
models.UniqueConstraint(
fields=['proyecto_id', 'subapartado_id'], name="unique-proyecto-subapartado"
)
]
verbose_name = _('respuesta de la memoria')
verbose_name_plural = _('respuestas de la memoria')
def __str__(self):
return self.texto
@classmethod
def get_or_create(cls, proyecto_id, subapartado_id):
"""Devuelve la respuesta al supapartado de la memoria y proyecto indicados."""
try:
respuesta = MemoriaRespuesta.objects.get(
proyecto_id=proyecto_id, subapartado_id=subapartado_id
)
except MemoriaRespuesta.DoesNotExist:
respuesta = MemoriaRespuesta.objects.create(
proyecto_id=proyecto_id, subapartado_id=subapartado_id
)
return respuesta
class Opcion(models.Model):
"""Respuestas posibles a los criterios, cada una con una puntuación."""
......
......@@ -10,6 +10,7 @@ from .views import (
InvitacionView,
MemoriaDetailView,
MemoriaPresentarView,
MemoriaUpdateFieldView,
ParticipanteAceptarView,
ParticipanteDeclinarView,
ParticipanteDeleteView,
......@@ -102,6 +103,11 @@ urlpatterns = [
name='participante_delete',
),
path('memoria/<int:pk>/', MemoriaDetailView.as_view(), name='memoria_detail'),
path(
'memoria/<int:proyecto_id>/edit/<int:sub_pk>/',
MemoriaUpdateFieldView.as_view(),
name='memoria_update_field',
),
path('memoria/<int:pk>/presentar/', MemoriaPresentarView.as_view(), name='memoria_presentar'),
path('proyecto/new/', ProyectoCreateView.as_view(), name='proyecto_new'),
path('proyecto/<int:pk>/', ProyectoDetailView.as_view(), name='proyecto_detail'),
......
# Standard library
import csv
import json
import magic
# import magic
from datetime import date
from os.path import splitext
# from os.path import splitext
# Third-party
from annoying.functions import get_config, get_object_or_None
......@@ -37,15 +39,20 @@ from django.views.generic import DetailView, RedirectView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
# Local Django
from .forms import EvaluadorForm, InvitacionForm, ProyectoForm, ResolucionForm
from .forms import (
EvaluadorForm,
InvitacionForm,
MemoriaRespuestaForm,
ProyectoForm,
ResolucionForm,
)
from .models import (
Centro,
Convocatoria,
Criterio,
Evento,
MemoriaApartado,
MemoriaRespuesta,
MemoriaSubapartado,
# MemoriaSubapartado,
ParticipanteProyecto,
Plan,
Proyecto,
......@@ -614,8 +621,14 @@ class MemoriaDetailView(LoginRequiredMixin, ChecksMixin, TemplateView):
context['proyecto'] = proyecto
context['apartados'] = proyecto.convocatoria.apartados_memoria.all()
context['dict_respuestas'] = proyecto.get_dict_respuestas_memoria()
context['permitir_edicion'] = (
self.es_coordinador(proyecto.id) and proyecto.estado == 'ACEPTADO'
)
return context
"""
def post(self, request, *args, **kwargs):
proyecto = get_object_or_404(Proyecto, pk=kwargs['pk'])
subapartados = MemoriaSubapartado.objects.filter(
......@@ -654,6 +667,7 @@ class MemoriaDetailView(LoginRequiredMixin, ChecksMixin, TemplateView):
request, _(f'Se ha guardado la memoria del proyecto «{proyecto.titulo}».')
)
return redirect('proyecto_detail', proyecto.id)
"""
def test_func(self):
# TODO: Comprobar fecha y estado
......@@ -674,6 +688,29 @@ class MemoriaPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView):
return self.es_coordinador(self.kwargs['pk'])
class MemoriaUpdateFieldView(LoginRequiredMixin, ChecksMixin, UpdateView):
"""Actualiza la respuesta a un subapartado de una memoria."""
model = MemoriaRespuesta
template_name = 'memoria/update.html'
form_class = MemoriaRespuestaForm # Definido en `forms.py`
def get_object(self, queryset=None):
"""Return the object the view is displaying."""
proyecto_id = self.kwargs.get('proyecto_id')
subapartado_id = self.kwargs.get('sub_pk')
return MemoriaRespuesta.get_or_create(proyecto_id, subapartado_id)
def get_success_url(self):
return reverse_lazy('memoria_detail', args=[self.object.proyecto_id])
def test_func(self):
# TODO: Comprobar estado del proyecto, fecha.
return self.es_coordinador(self.kwargs['proyecto_id']) or self.request.user.has_perm(
'indo.editar_proyecto'
)
class ProyectoDetailView(LoginRequiredMixin, ChecksMixin, DetailView):
"""Muestra una solicitud de proyecto."""
......
......@@ -351,7 +351,7 @@ ALLOWED_ATTRIBUTES = {
'acronym': ['title'],
'img': ['alt', 'src'],
}
ALLOWED_STYLES = ['background-color', 'color', 'text-align']
ALLOWED_STYLES = ['background-color', 'color', 'text-align', 'width']
ALLOWED_PROTOCOLS = ['data', 'http', 'https', 'mailto']
X_FRAME_OPTIONS = 'SAMEORIGIN' # Required by SummernoteWidget on Django 3.x
......
......@@ -15,7 +15,6 @@
<span class="fas fa-info-circle"></span>
{% trans 'Aquí puede ver y editar la memoria del proyecto.' %}<br />
{% trans 'Pulse el botón «<strong>Guardar</strong>» para almacenar sus cambios.' %}
{% trans 'Puede editar su memoria tantas veces como desee.' %}<br />
{% trans 'Cuando esté satisfecho, pulse el botón «<strong>Presentar</strong>».' %}
{% trans 'Una vez haya presentado la memoria para su corrección, ya no podrá modificarla.' %}<br /><br />
......@@ -149,8 +148,8 @@
</div>
<br />
<form action="" enctype="multipart/form-data" method="post">
{% csrf_token %}
{# <form action="" enctype="multipart/form-data" method="post"> #}
{# {% csrf_token %} #}
{% for apartado in apartados %}
<h3>{{ apartado.numero }}. {{ apartado.descripcion }}</h3>
......@@ -160,6 +159,7 @@
<p class="noprint" style="color: gray;">{{ subapartado.ayuda }}</p>
{% with respuesta=dict_respuestas|get_item:subapartado.id %}
{% comment %}
{% if subapartado.tipo == 'texto' %}
<textarea class="textarea form-control" name="{{ subapartado.id }}"
placeholder="{% trans 'Introduzca sus comentarios.' %}" rows="10"
......@@ -170,6 +170,15 @@
{% endif %}
<input type="file" name="fichero" accept=".pdf">
{% endif %}
{% endcomment %}
<p>{{ respuesta.texto | default:'' | limpiar }}</p>
{% if permitir_edicion %}
<p>
<a href="{% url 'memoria_update_field' proyecto.id subapartado.id %}" class="btn btn-info btn-sm">
<span class="fas fa-pencil-alt" aria-hidden="true"></span>&nbsp; {% trans 'Editar' %}
</a>
</p>
{% endif %}
{% endwith %}
<br />
......@@ -178,25 +187,28 @@
<!-- Botones -->
<br style="clear: both;" />
<div class="btn-group noprint" role="group" aria-label="Botones">
<a href="{% url 'proyecto_detail' proyecto.id %}" class="btn btn-warning"
title="{% trans 'Cancelar - Se perderán los cambios no guardados' %}">
<span class="fas fa-times"></span> {% trans 'Cancelar' %}
<div class="btn-group noprint" role="group" aria-label="{{ _('Botones') }}">
<a href="{% url 'proyecto_detail' proyecto.id %}" class="btn btn-info"
title="{% trans 'Volver al proyecto' %}">
<span class="fas fa-step-backward"></span> {% trans 'Retroceder' %}
</a>
{# TODO if permitir_edicion #}
{% comment %}
{% if permitir_edicion %}
<button class="btn btn-success" type="submit" title="{% trans 'Guardar cambios' %}">
<span class="far fa-save"></span> {% trans 'Guardar' %}
</button>
{# endif #}
{# TODO if es_coordinador #}
{% endif %}
{% endcomment %}
{% if permitir_edicion %}
<button class="btn btn-danger" data-toggle="modal" data-target="#presentarModal"
type="button">
<span class="fas fa-file-export"></span> {% trans 'Presentar' %}
</button>
{# endif #}
{% endif %}
</div>
</form>
{# </form> #}
</div>
<div class="modal fade" id="presentarModal" tabindex="-1" role="dialog"
......
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans 'Actualizar memoria de proyecto' %}{% endblock title %}
{% block description %}{% trans 'Actualizar memoria de proyecto' %}{% endblock description %}
{% block content %}
<div class="container-blanco">
<h1>{% trans 'Actualizar memoria de proyecto' %}</h1>
<h2><small>{{ object.proyecto.titulo }}</small></h2>
<hr />
<br />
<div class="alert-info alert fade-in noprint">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<span class="fas fa-info-circle"></span>
{% trans 'Aquí puede ver y editar este apartado de la memoria del proyecto.' %}<br />
{% trans 'Si lo precisa, puede insertar imágenes y tablas.' %}<br />
{% trans 'Hasta que presente la memoria, puede editar el campo tantas veces como desee.' %}
{% trans 'Una vez haya presentado la memoria para su corrección, ya no podrá modificarlo.' %}<br /><br />
</div>
<br />
<h3>{{ object.subapartado.descripcion }}</h3>
<form action="" method="post">
{% csrf_token %}
{# MemoriaRespuestaForm está definido en forms.py #}
{{ form.as_p }}
<br style="clear: both;" />
<div class="btn-group" role="group" aria-label="{{_('Botones')}}">
<a href="{% url 'memoria_detail' object.proyecto_id %}" class="btn btn-info"
title="{% trans 'Cancelar - Se perderán los cambios no guardados' %}">
<span class="fas fa-times"></span> {% trans 'Cancelar' %}
</a>
<button class="btn btn-warning" type="submit">
<span class="fas fa-check"></span> {% trans 'Actualizar' %}
</button>
</div>
</form>
</div>
{% endblock content %}
{% extends 'base.html' %}
{% load custom_tags i18n %}
{% block title %}{% trans 'Solicitud de proyecto' %}: {{ proyecto.titulo }}{% endblock title %}
{% block content %}
<div class="container-blanco">
{% if permitir_edicion %}
......
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