From 4b1e4c2bd48212553978f12bef5c0058a27c8a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Mat=C3=ADas=20S=C3=A1nchez=20=28Quique=29?= Date: Fri, 28 Feb 2020 14:56:56 +0100 Subject: [PATCH] =?UTF-8?q?style=20Seguir=20las=20directrices=20de=20estil?= =?UTF-8?q?o=20de=20c=C3=B3digo=20de=20Django?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use single quotes for strings, or a double quote if the string contains a single quote. * An exception to PEP 8 is our rules on line lengths. Don’t limit lines of code to 79 characters if it means the code looks significantly uglier or is harder to read. We allow up to 119 characters as this is the width of GitHub code review; anything longer requires horizontal scrolling which makes review more difficult. Ver --- .flake8 | 6 + accounts/admin.py | 7 +- accounts/apps.py | 2 +- accounts/migrations/0001_initial.py | 115 +-- .../migrations/0002_auto_20190314_0819.py | 66 +- .../migrations/0003_auto_20190531_1456.py | 7 +- accounts/models.py | 50 +- accounts/pipeline.py | 20 +- accounts/urls.py | 14 +- accounts/views.py | 51 +- indo/apps.py | 2 +- indo/forms.py | 109 +-- indo/migrations/0001_initial.py | 818 ++++++------------ indo/migrations/0002_auto_20200131_1244.py | 35 +- indo/migrations/0003_auto_20200131_1245.py | 23 +- indo/migrations/0004_auto_20200131_1409.py | 27 +- indo/migrations/0005_auto_20200203_1316.py | 40 +- indo/migrations/0006_auto_20200207_0919.py | 32 +- indo/models.py | 475 ++++------ indo/tables.py | 22 +- indo/templatetags/custom_tags.py | 44 +- indo/tests.py | 30 +- indo/urls.py | 66 +- indo/views.py | 490 ++++------- manage.py | 8 +- manhattan_project/settings-sample.py | 339 ++++---- manhattan_project/urls.py | 30 +- manhattan_project/wsgi.py | 6 +- pyproject.toml | 24 + 29 files changed, 1101 insertions(+), 1857 deletions(-) create mode 100644 .flake8 create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..325498a --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +exclude=**/settings*.py,**/__init__.py,**/migrations/*.py +ignore = W503 +max-line-length = 119 +max-complexity = 10 +select = B,C,E,F,W diff --git a/accounts/admin.py b/accounts/admin.py index 5a9666d..3d17aa3 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -9,12 +9,7 @@ class CustomUserAdmin(UserAdmin): add_form = CustomUserCreationForm form = CustomUserChangeForm model = CustomUser - list_display = [ - "username", - "email", - "first_name", - "last_name", - ] # Campos que se muestran en el listado + list_display = ['username', 'email', 'first_name', 'last_name'] # Campos que se muestran en el listado admin.site.register(CustomUser, CustomUserAdmin) diff --git a/accounts/apps.py b/accounts/apps.py index fb0257e..9b3fc5a 100644 --- a/accounts/apps.py +++ b/accounts/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class AccountsConfig(AppConfig): - name = "accounts" + name = 'accounts' diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index d4d2785..e0805c6 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -10,119 +10,78 @@ class Migration(migrations.Migration): initial = True - dependencies = [("auth", "0009_alter_user_last_name_max_length")] + dependencies = [('auth', '0009_alter_user_last_name_max_length')] operations = [ migrations.CreateModel( - name="CustomUser", + name='CustomUser', fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("password", models.CharField(max_length=128, verbose_name="password")), - ( - "last_login", - models.DateTimeField( - blank=True, null=True, verbose_name="last login" - ), - ), - ( - "is_superuser", + 'is_superuser', models.BooleanField( default=False, - help_text="Designates that this user has all permissions without explicitly assigning them.", - verbose_name="superuser status", + help_text='Designates that this user has all permissions without explicitly assigning them.', + verbose_name='superuser status', ), ), ( - "username", + 'username', models.CharField( - error_messages={ - "unique": "A user with that username already exists." - }, - help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + error_messages={'unique': 'A user with that username already exists.'}, + help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, - validators=[ - django.contrib.auth.validators.UnicodeUsernameValidator() - ], - verbose_name="username", - ), - ), - ( - "first_name", - models.CharField( - blank=True, max_length=30, verbose_name="first name" + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name='username', ), ), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ( - "last_name", - models.CharField( - blank=True, max_length=150, verbose_name="last name" - ), - ), - ( - "email", - models.EmailField( - blank=True, max_length=254, verbose_name="email address" - ), - ), - ( - "is_staff", + 'is_staff', models.BooleanField( default=False, - help_text="Designates whether the user can log into this admin site.", - verbose_name="staff status", + help_text='Designates whether the user can log into this admin site.', + verbose_name='staff status', ), ), ( - "is_active", + 'is_active', models.BooleanField( default=True, - help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", - verbose_name="active", - ), - ), - ( - "date_joined", - models.DateTimeField( - default=django.utils.timezone.now, verbose_name="date joined" + help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', + verbose_name='active', ), ), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ( - "groups", + 'groups', models.ManyToManyField( blank=True, - help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", - related_name="user_set", - related_query_name="user", - to="auth.Group", - verbose_name="groups", + help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', + related_name='user_set', + related_query_name='user', + to='auth.Group', + verbose_name='groups', ), ), ( - "user_permissions", + 'user_permissions', models.ManyToManyField( blank=True, - help_text="Specific permissions for this user.", - related_name="user_set", - related_query_name="user", - to="auth.Permission", - verbose_name="user permissions", + help_text='Specific permissions for this user.', + related_name='user_set', + related_query_name='user', + to='auth.Permission', + verbose_name='user permissions', ), ), ], - options={ - "verbose_name": "user", - "verbose_name_plural": "users", - "abstract": False, - }, - managers=[("objects", django.contrib.auth.models.UserManager())], + options={'verbose_name': 'user', 'verbose_name_plural': 'users', 'abstract': False}, + managers=[('objects', django.contrib.auth.models.UserManager())], ) ] diff --git a/accounts/migrations/0002_auto_20190314_0819.py b/accounts/migrations/0002_auto_20190314_0819.py index 56a12c9..277cce6 100644 --- a/accounts/migrations/0002_auto_20190314_0819.py +++ b/accounts/migrations/0002_auto_20190314_0819.py @@ -5,77 +5,63 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("accounts", "0001_initial")] + dependencies = [('accounts', '0001_initial')] operations = [ migrations.AddField( - model_name="customuser", - name="centro_id_nks", - field=models.CharField( - blank=True, max_length=127, null=True, verbose_name="Cód. centros" - ), + model_name='customuser', + name='centro_id_nks', + field=models.CharField(blank=True, max_length=127, null=True, verbose_name='Cód. centros'), ), migrations.AddField( - model_name="customuser", - name="colectivos", - field=models.CharField(blank=True, max_length=127, null=True), + model_name='customuser', name='colectivos', field=models.CharField(blank=True, max_length=127, null=True) ), migrations.AddField( - model_name="customuser", - name="departamento_id_nks", - field=models.CharField( - blank=True, max_length=127, null=True, verbose_name="Cód. departamentos" - ), + model_name='customuser', + name='departamento_id_nks', + field=models.CharField(blank=True, max_length=127, null=True, verbose_name='Cód. departamentos'), ), migrations.AddField( - model_name="customuser", - name="last_name_2", - field=models.CharField( - blank=True, max_length=150, null=True, verbose_name="segundo apellido" - ), + model_name='customuser', + name='last_name_2', + field=models.CharField(blank=True, max_length=150, null=True, verbose_name='segundo apellido'), ), migrations.AddField( - model_name="customuser", - name="nombre_oficial", + model_name='customuser', + name='nombre_oficial', field=models.CharField(blank=True, max_length=50, null=True), ), migrations.AddField( - model_name="customuser", - name="numero_documento", + model_name='customuser', + name='numero_documento', field=models.CharField( blank=True, - help_text="DNI, NIE o pasaporte.", + help_text='DNI, NIE o pasaporte.', max_length=16, null=True, - verbose_name="número de documento", + verbose_name='número de documento', ), ), migrations.AddField( - model_name="customuser", - name="sexo", - field=models.CharField(blank=True, max_length=1, null=True), + model_name='customuser', name='sexo', field=models.CharField(blank=True, max_length=1, null=True) ), migrations.AddField( - model_name="customuser", - name="sexo_oficial", - field=models.CharField(blank=True, max_length=1, null=True), + model_name='customuser', name='sexo_oficial', field=models.CharField(blank=True, max_length=1, null=True) ), migrations.AddField( - model_name="customuser", - name="tipo_documento", + model_name='customuser', + name='tipo_documento', field=models.CharField( blank=True, - help_text="DNI, NIE o pasaporte.", + help_text='DNI, NIE o pasaporte.', max_length=3, null=True, - verbose_name="tipo de documento", + verbose_name='tipo de documento', ), ), migrations.AlterField( - model_name="customuser", - name="first_name", - field=models.CharField( - blank=True, max_length=50, verbose_name="first name" - ), + model_name='customuser', + name='first_name', + field=models.CharField(blank=True, max_length=50, verbose_name='first name'), ), ] diff --git a/accounts/migrations/0003_auto_20190531_1456.py b/accounts/migrations/0003_auto_20190531_1456.py index d626395..40c3ae4 100644 --- a/accounts/migrations/0003_auto_20190531_1456.py +++ b/accounts/migrations/0003_auto_20190531_1456.py @@ -6,11 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [("accounts", "0002_auto_20190314_0819")] + dependencies = [('accounts', '0002_auto_20190314_0819')] operations = [ - migrations.AlterModelManagers( - name="customuser", - managers=[("objects", accounts.models.CustomUserManager())], - ) + migrations.AlterModelManagers(name='customuser', managers=[('objects', accounts.models.CustomUserManager())]) ] diff --git a/accounts/models.py b/accounts/models.py index d92f16c..2a85c6b 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -7,10 +7,10 @@ from django.utils.translation import gettext_lazy as _ class CustomUserManager(UserManager): def get_or_none(self, **kwargs): - """Devuelve el usuario con las propiedades indicadas. + '''Devuelve el usuario con las propiedades indicadas. Si no se encuentra, devuelve `None`. - """ + ''' try: return self.get(**kwargs) except CustomUser.DoesNotExist: @@ -19,42 +19,26 @@ class CustomUserManager(UserManager): class CustomUser(AbstractUser): # Campos sobrescritos - first_name = models.CharField( - _("first name"), max_length=50, blank=True # era: max_length=30 - ) + first_name = models.CharField(_('first name'), max_length=50, blank=True) # era: max_length=30 # Campos adicionales numero_documento = models.CharField( - _("número de documento"), - max_length=16, - blank=True, - null=True, - help_text=_("DNI, NIE o pasaporte."), + _('número de documento'), max_length=16, blank=True, null=True, help_text=_('DNI, NIE o pasaporte.') ) tipo_documento = models.CharField( - _("tipo de documento"), - max_length=3, - blank=True, - null=True, - help_text=_("DNI, NIE o pasaporte."), - ) - last_name_2 = models.CharField( - _("segundo apellido"), max_length=150, blank=True, null=True + _('tipo de documento'), max_length=3, blank=True, null=True, help_text=_('DNI, NIE o pasaporte.') ) + last_name_2 = models.CharField(_('segundo apellido'), max_length=150, blank=True, null=True) sexo = models.CharField(max_length=1, blank=True, null=True) sexo_oficial = models.CharField(max_length=1, blank=True, null=True) nombre_oficial = models.CharField(max_length=50, blank=True, null=True) - centro_id_nks = models.CharField( - _("Cód. centros"), max_length=127, blank=True, null=True - ) - departamento_id_nks = models.CharField( - _("Cód. departamentos"), max_length=127, blank=True, null=True - ) + centro_id_nks = models.CharField(_('Cód. centros'), max_length=127, blank=True, null=True) + departamento_id_nks = models.CharField(_('Cód. departamentos'), max_length=127, blank=True, null=True) colectivos = models.CharField(max_length=127, blank=True, null=True) # Metodos sobrescritos def get_full_name(self): - """Devuelve el nombre completo (nombre y los dos apellidos).""" - full_name = "%s %s %s" % (self.first_name, self.last_name, self.last_name_2) + '''Devuelve el nombre completo (nombre y los dos apellidos).''' + full_name = '%s %s %s' % (self.first_name, self.last_name, self.last_name_2) return full_name.strip() # Métodos adicionales @@ -62,25 +46,25 @@ class CustomUser(AbstractUser): return self.username def get_colectivo_principal(self): - """Devuelve el colectivo principal del usuario. + '''Devuelve el colectivo principal del usuario. Se determina usando el orden de prelación PDI > ADS > PAS > EST. - """ + ''' colectivos_del_usuario = json.loads(self.colectivos) if self.colectivos else [] - for col in ("PDI", "ADS", "PAS", "EST"): + for col in ('PDI', 'ADS', 'PAS', 'EST'): if col in colectivos_del_usuario: return col return None def get_num_equipos(self, anyo): - """Devuelve el número de equipos de trabajo en los que participa el usuario.""" + '''Devuelve el número de equipos de trabajo en los que participa el usuario.''' num_como_participante = self.vinculaciones.filter( - tipo_participacion="participante", proyecto__convocatoria_id=anyo + tipo_participacion='participante', proyecto__convocatoria_id=anyo ).count() num_como_coordinador = self.vinculaciones.filter( - tipo_participacion__in=["coordinador", "coordinador_principal"], + tipo_participacion__in=['coordinador', 'coordinador_principal'], proyecto__convocatoria_id=anyo, - proyecto__estado="SOLICITADO", + proyecto__estado='SOLICITADO', ).count() num_equipos = num_como_participante + num_como_coordinador return num_equipos diff --git a/accounts/pipeline.py b/accounts/pipeline.py index 86eb642..ce619bb 100644 --- a/accounts/pipeline.py +++ b/accounts/pipeline.py @@ -14,18 +14,14 @@ from django.core.validators import ValidationError, validate_email def get_identidad(strategy, response, user, *args, **kwargs): - """Actualiza el usuario con los datos obtenidos de Gestión de Identidades.""" + '''Actualiza el usuario con los datos obtenidos de Gestión de Identidades.''' wsdl = get_config('WSDL_IDENTIDAD') session = Session() - session.auth = HTTPBasicAuth( - get_config("USER_IDENTIDAD"), get_config("PASS_IDENTIDAD") - ) + session.auth = HTTPBasicAuth(get_config('USER_IDENTIDAD'), get_config('PASS_IDENTIDAD')) try: - client = zeep.Client( - wsdl=wsdl, transport=zeep.transports.Transport(session=session) - ) + client = zeep.Client(wsdl=wsdl, transport=zeep.transports.Transport(session=session)) except RequestConnectionError: raise RequestConnectionError('No fue posible conectarse al WS de Identidades.') except: # noqa: E722 @@ -44,12 +40,8 @@ def get_identidad(strategy, response, user, *args, **kwargs): user.first_name = identidad.nombre user.last_name = identidad.primerApellido user.last_name_2 = identidad.segundoApellido - correo_personal = ( - identidad.correoPersonal if is_email_valid(identidad.correoPersonal) else None - ) - correo_principal = ( - identidad.correoPrincipal if is_email_valid(identidad.correoPrincipal) else None - ) + correo_personal = identidad.correoPersonal if is_email_valid(identidad.correoPersonal) else None + correo_principal = identidad.correoPrincipal if is_email_valid(identidad.correoPrincipal) else None # El email es un campo NOT NULL en el modelo. user.email = correo_personal or correo_principal or '' user.is_active = identidad.activo != 'N' @@ -71,7 +63,7 @@ def get_identidad(strategy, response, user, *args, **kwargs): def is_email_valid(email): - """Validate email address""" + '''Validate email address''' try: validate_email(email) except ValidationError: diff --git a/accounts/urls.py b/accounts/urls.py index a52055a..727ef39 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -6,17 +6,13 @@ from . import views urlpatterns = [ # Evita que un usuario ya autenticado pueda volver a la página de inicio de sesión - path( - "login/", - auth_views.LoginView.as_view(redirect_authenticated_user=True), - name="login", - ), + path('login/', auth_views.LoginView.as_view(redirect_authenticated_user=True), name='login'), # Logout personalizado para solicitar el fin de la sesión SAML. - path("logout/", views.LogoutView.as_view(), name="logout"), + path('logout/', views.LogoutView.as_view(), name='logout'), # Finalizar la sesión SAML. - path("sls/", views.SlsView.as_view(), name="sls"), + path('sls/', views.SlsView.as_view(), name='sls'), # Muestra los metadatos para el Proveedor de Identidad (IdP) de SAML. - path("metadata", views.metadata_xml, name="metadata"), + path('metadata', views.metadata_xml, name='metadata'), # Muestra los datos del usuario. - path("userdata", views.UserdataView.as_view(), name="userdata"), + path('userdata', views.UserdataView.as_view(), name='userdata'), ] diff --git a/accounts/views.py b/accounts/views.py index 06c762c..e961fcc 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -11,74 +11,67 @@ from social_django.utils import load_backend, load_strategy def metadata_xml(request): - """Muestra los metadatos para el Proveedor de Identidad (IdP) de SAML.""" - complete_url = reverse("social:complete", args=("saml",)) - saml_backend = load_backend( - load_strategy(request), "saml", redirect_uri=complete_url - ) + '''Muestra los metadatos para el Proveedor de Identidad (IdP) de SAML.''' + complete_url = reverse('social:complete', args=('saml',)) + saml_backend = load_backend(load_strategy(request), 'saml', redirect_uri=complete_url) metadata, errors = saml_backend.generate_metadata_xml() if errors: - raise Exception("\n".join(errors)) - return HttpResponse(content=metadata, content_type="text/xml") + raise Exception('\n'.join(errors)) + return HttpResponse(content=metadata, content_type='text/xml') class UserdataView(LoginRequiredMixin, View): - """Muestra los datos del usuario.""" + '''Muestra los datos del usuario.''' def get(self, request, *args, **kwargs): context = {} - context["datos_usuario"] = { - field.name: field.value_to_string(request.user) - for field in request.user._meta.fields + context['datos_usuario'] = { + field.name: field.value_to_string(request.user) for field in request.user._meta.fields } - return render(request, "registration/userdata.html", context=context) + return render(request, 'registration/userdata.html', context=context) -@method_decorator(never_cache, name="dispatch") +@method_decorator(never_cache, name='dispatch') class LogoutView(LoginRequiredMixin, RedirectView): - """Log out the current user. + '''Log out the current user. This view logs out a locally authenticated user, or sends a Logout request to the SAML2 Identity Provider. - """ + ''' - url = reverse_lazy(get_config("LOGOUT_REDIRECT_URL")) + url = reverse_lazy(get_config('LOGOUT_REDIRECT_URL')) def get(self, request, *args, **kwargs): - saml_backend = load_backend( - load_strategy(request), "saml", redirect_uri="{% url 'sls' %}" - ) + saml_backend = load_backend(load_strategy(request), 'saml', redirect_uri="{% url 'sls' %}") # As of now, this code only handles the first association. association = request.user.social_auth.first() if association: - idp_name = association.uid.split(":")[0] + idp_name = association.uid.split(':')[0] sls_url = saml_backend.request_logout(idp_name, association) return redirect(sls_url) logout(request) return super().get(request, *args, **kwargs) -@method_decorator(never_cache, name="dispatch") +@method_decorator(never_cache, name='dispatch') class SlsView(View): - """ + ''' The Single Logout Service processes the Logout response from the Identity Provider. The logout request may have been generated by this Service Provider, or other SP. - """ + ''' - url = reverse_lazy(get_config("LOGOUT_REDIRECT_URL")) + url = reverse_lazy(get_config('LOGOUT_REDIRECT_URL')) def get(self, request, *args, **kwargs): - saml_backend = load_backend( - load_strategy(request), "saml", redirect_uri=str(self.url) - ) + saml_backend = load_backend(load_strategy(request), 'saml', redirect_uri=str(self.url)) # As of now, this code only handles the first association. association = request.user.social_auth.first() - idp_name = association.uid.split(":")[0] + idp_name = association.uid.split(':')[0] _, errors = saml_backend.process_logout(idp_name, None) if errors: - messages = "\n".join(errors) + messages = '\n'.join(errors) raise Exception(messages) logout(request) diff --git a/indo/apps.py b/indo/apps.py index ea79bad..ce32f6e 100644 --- a/indo/apps.py +++ b/indo/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class IndoConfig(AppConfig): - name = "indo" + name = 'indo' diff --git a/indo/forms.py b/indo/forms.py index 246ee26..0b92c70 100644 --- a/indo/forms.py +++ b/indo/forms.py @@ -13,21 +13,19 @@ from .models import Linea, ParticipanteProyecto, Programa, Proyecto, TipoPartici 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." - ), + label=_('NIP'), + help_text=_('Número de Identificación Personal en la Universidad de Zaragoza de la persona a invitar.'), ) def __init__(self, *args, **kwargs): # Override __init__ to make the "self" object have the proyecto instance # designated by the proyecto_id sent by the view, taken from the URL parameter. - self.proyecto = Proyecto.objects.get(id=kwargs.pop("proyecto_id")) - self.request = kwargs.pop("request") + self.proyecto = Proyecto.objects.get(id=kwargs.pop('proyecto_id')) + self.request = kwargs.pop('request') super().__init__(*args, **kwargs) def _crear_usuario(self, nip): - """Crea un registro de usuario con el nip indicado y los datos de G.I.""" + '''Crea un registro de usuario con el nip indicado y los datos de G.I.''' usuario = CustomUser.objects.create_user(username=nip) try: @@ -36,19 +34,17 @@ class InvitacionForm(forms.ModelForm): # Si Gestión de Identidades devuelve un error, borramos el usuario # y finalizamos mostrando el mensaje de error. usuario.delete() - raise forms.ValidationError("ERROR: " + str(ex)) + raise forms.ValidationError('ERROR: ' + str(ex)) # HACK - Indicamos que la autenticación es vía Single Sign On con SAML. - usuario_social = UserSocialAuth( - uid=f"lord:{usuario.username}", provider="saml", user=usuario - ) + usuario_social = UserSocialAuth(uid=f'lord:{usuario.username}', provider='saml', user=usuario) usuario_social.save() return usuario def clean(self): cleaned_data = super().clean() - nip = cleaned_data.get("nip") + nip = cleaned_data.get('nip') # Comprobamos si el usuario ya existe en el sistema. usuario = CustomUser.objects.get_or_none(username=nip) @@ -65,112 +61,83 @@ class InvitacionForm(forms.ModelForm): # Si el usuario no está activo, finalizamos explicando esta circunstancia. if not usuario.is_active: - raise forms.ValidationError( - _("Usuario inactivo en el sistema de Gestión de Identidades") - ) + raise forms.ValidationError(_('Usuario inactivo en el sistema de Gestión de Identidades')) # Si el usuario no tiene un email válido, finalizamos explicando esta circunstancia. if not usuario.email: raise forms.ValidationError( _( - f"No fue posible invitar al usuario «{nip}» porque no tiene " - "establecida ninguna dirección de correo electrónico en el sistema " - "de Gestión de Identidades." + f'No fue posible invitar al usuario «{nip}» porque no tiene ' + 'establecida ninguna dirección de correo electrónico en el sistema ' + 'de Gestión de Identidades.' ) ) - cleaned_data["usuario"] = usuario + cleaned_data['usuario'] = usuario # Si un usuario ya está vinculado al proyecto, no se le puede invitar. vinculados = self.proyecto.get_usuarios_vinculados() if usuario in vinculados: raise forms.ValidationError( - _( - f"No puede invitar a {usuario.get_full_name()} " - "porque ya está vinculado a este proyecto." - ) + _(f'No puede invitar a {usuario.get_full_name()} ' 'porque ya está vinculado a este proyecto.') ) # La participación de los estudiantes estará limitada a dos por proyecto # (excepto en los PIPOUZ). - if ( - self.proyecto.programa.nombre_corto != "PIPOUZ" - and usuario.get_colectivo_principal() == "EST" - ): - estudiantes = [ - vinculado - for vinculado in vinculados - if vinculado.get_colectivo_principal() == "EST" - ] + if self.proyecto.programa.nombre_corto != 'PIPOUZ' and usuario.get_colectivo_principal() == 'EST': + estudiantes = [vinculado for vinculado in vinculados if vinculado.get_colectivo_principal() == 'EST'] if len(estudiantes) >= self.proyecto.programa.max_estudiantes: - nombres_estudiantes = ", ".join( - list(map(lambda e: e.get_full_name(), estudiantes)) - ) + nombres_estudiantes = ', '.join(list(map(lambda e: e.get_full_name(), estudiantes))) raise forms.ValidationError( _( - "Ya se ha alcanzado el máximo de participación de " - f"{self.proyecto.programa.max_estudiantes} estudiantes " - f"por proyecto: {nombres_estudiantes}." + 'Ya se ha alcanzado el máximo de participación de ' + f'{self.proyecto.programa.max_estudiantes} estudiantes ' + f'por proyecto: {nombres_estudiantes}.' ) ) def save(self, commit=True): invitado = super().save(commit=False) invitado.proyecto = self.proyecto - invitado.tipo_participacion = TipoParticipacion("invitado") - invitado.usuario = self.cleaned_data["usuario"] + invitado.tipo_participacion = TipoParticipacion('invitado') + invitado.usuario = self.cleaned_data['usuario'] return invitado.save() class Meta: - fields = ["nip"] + fields = ['nip'] model = ParticipanteProyecto class ProyectoForm(forms.ModelForm): def clean(self): cleaned_data = super().clean() - programa = cleaned_data.get("programa") - linea = cleaned_data.get("linea") + programa = cleaned_data.get('programa') + linea = cleaned_data.get('linea') lineas_del_programa = programa.lineas.all() - centro = cleaned_data.get("centro") - estudio = cleaned_data.get("estudio") + centro = cleaned_data.get('centro') + estudio = cleaned_data.get('estudio') if linea and linea.programa_id != programa.id: - self.add_error( - "linea", - _("La línea seleccionada no pertenece al programa seleccionado."), - ) + self.add_error('linea', _('La línea seleccionada no pertenece al programa seleccionado.')) if lineas_del_programa and not linea: - self.add_error("linea", _("Este programa requiere seleccionar una línea.")) + self.add_error('linea', _('Este programa requiere seleccionar una línea.')) - if programa.nombre_corto in ("PIEC", "PRACUZ", "PIPOUZ") and not centro: - self.add_error( - "centro", _("Este programa debe estar vinculado a un centro.") - ) + if programa.nombre_corto in ('PIEC', 'PRACUZ', 'PIPOUZ') and not centro: + self.add_error('centro', _('Este programa debe estar vinculado a un centro.')) - if programa.nombre_corto == "PIET" and not estudio: - self.add_error( - "estudio", _("Los PIET deben estar vinculados a un estudio.") - ) + if programa.nombre_corto == 'PIET' and not estudio: + self.add_error('estudio', _('Los PIET deben estar vinculados a un estudio.')) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["programa"].widget.choices = tuple( + self.fields['programa'].widget.choices = tuple( BLANK_CHOICE_DASH - + list( - Programa.objects.filter(convocatoria_id=date.today().year) - .values_list("id", "nombre_corto") - .all() - ) + + list(Programa.objects.filter(convocatoria_id=date.today().year).values_list('id', 'nombre_corto').all()) ) - self.fields["linea"].widget.choices = tuple( + self.fields['linea'].widget.choices = tuple( BLANK_CHOICE_DASH - + list( - Linea.objects.filter(programa__convocatoria_id=date.today().year) - .values_list("id", "nombre") - .all() - ) + + list(Linea.objects.filter(programa__convocatoria_id=date.today().year).values_list('id', 'nombre').all()) ) class Meta: - fields = ["titulo", "descripcion", "programa", "linea", "centro", "estudio"] + fields = ['titulo', 'descripcion', 'programa', 'linea', 'centro', 'estudio'] model = Proyecto diff --git a/indo/migrations/0001_initial.py b/indo/migrations/0001_initial.py index c9ab45c..01e2995 100644 --- a/indo/migrations/0001_initial.py +++ b/indo/migrations/0001_initial.py @@ -13,909 +13,585 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="Centro", + name='Centro', fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ( - "academico_id_nk", - models.IntegerField( - blank=True, null=True, verbose_name="cód. académico" - ), - ), - ( - "rrhh_id_nk", - models.CharField( - blank=True, max_length=4, null=True, verbose_name="cód. RRHH" - ), - ), - ("nombre", models.CharField(max_length=255)), - ( - "tipo_centro", - models.CharField( - blank=True, - max_length=30, - null=True, - verbose_name="tipo de centro", - ), - ), - ( - "direccion", - models.CharField( - blank=True, max_length=140, null=True, verbose_name="dirección" - ), - ), - ("municipio", models.CharField(blank=True, max_length=100, null=True)), - ( - "telefono", - models.CharField( - blank=True, max_length=30, null=True, verbose_name="teléfono" - ), - ), - ( - "email", - models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="email address", - ), - ), - ( - "url", - models.URLField( - blank=True, max_length=255, null=True, verbose_name="URL" - ), - ), - ( - "nip_decano", - models.PositiveIntegerField( - blank=True, null=True, verbose_name="NIP del decano o director" - ), - ), - ( - "nombre_decano", + ('id', models.AutoField(primary_key=True, serialize=False)), + ('academico_id_nk', models.IntegerField(blank=True, null=True, verbose_name='cód. académico')), + ('rrhh_id_nk', models.CharField(blank=True, max_length=4, null=True, verbose_name='cód. RRHH')), + ('nombre', models.CharField(max_length=255)), + ('tipo_centro', models.CharField(blank=True, max_length=30, null=True, verbose_name='tipo de centro')), + ('direccion', models.CharField(blank=True, max_length=140, null=True, verbose_name='dirección')), + ('municipio', models.CharField(blank=True, max_length=100, null=True)), + ('telefono', models.CharField(blank=True, max_length=30, null=True, verbose_name='teléfono')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='email address')), + ('url', models.URLField(blank=True, max_length=255, null=True, verbose_name='URL')), + ( + 'nip_decano', + models.PositiveIntegerField(blank=True, null=True, verbose_name='NIP del decano o director'), + ), + ( + 'nombre_decano', models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="nombre del decano o director", + blank=True, max_length=255, null=True, verbose_name='nombre del decano o director' ), ), ( - "email_decano", + 'email_decano', models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="email del decano o director", + blank=True, max_length=254, null=True, verbose_name='email del decano o director' ), ), ( - "tratamiento_decano", + 'tratamiento_decano', models.CharField( - blank=True, - help_text="Decano/a ó director(a).", - max_length=25, - null=True, - verbose_name="cargo", + blank=True, help_text='Decano/a ó director(a).', max_length=25, null=True, verbose_name='cargo' ), ), ( - "nip_secretario", - models.PositiveIntegerField( - blank=True, null=True, verbose_name="NIP del secretario" - ), + 'nip_secretario', + models.PositiveIntegerField(blank=True, null=True, verbose_name='NIP del secretario'), ), ( - "nombre_secretario", - models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="nombre del secretario", - ), + 'nombre_secretario', + models.CharField(blank=True, max_length=255, null=True, verbose_name='nombre del secretario'), ), ( - "email_secretario", - models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="email del secretario", - ), + 'email_secretario', + models.EmailField(blank=True, max_length=254, null=True, verbose_name='email del secretario'), ), ( - "nips_coord_pou", + 'nips_coord_pou', models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="NIPs de los coordinadores POU", + blank=True, max_length=255, null=True, verbose_name='NIPs de los coordinadores POU' ), ), ( - "nombres_coords_pou", + 'nombres_coords_pou', models.CharField( - blank=True, - max_length=1023, - null=True, - verbose_name="nombres de los coordinadores POU", + blank=True, max_length=1023, null=True, verbose_name='nombres de los coordinadores POU' ), ), ( - "emails_coords_pou", + 'emails_coords_pou', models.CharField( - blank=True, - max_length=1023, - null=True, - verbose_name="emails de los coordinadores POU", + blank=True, max_length=1023, null=True, verbose_name='emails de los coordinadores POU' ), ), ( - "unidad_gasto", - models.CharField( - blank=True, - max_length=3, - null=True, - verbose_name="unidad de gasto", - ), - ), - ( - "esta_activo", - models.BooleanField(default=False, verbose_name="¿Activo?"), + 'unidad_gasto', + models.CharField(blank=True, max_length=3, null=True, verbose_name='unidad de gasto'), ), + ('esta_activo', models.BooleanField(default=False, verbose_name='¿Activo?')), ], - options={ - "ordering": ["nombre"], - "unique_together": {("academico_id_nk", "rrhh_id_nk")}, - }, + options={'ordering': ['nombre'], 'unique_together': {('academico_id_nk', 'rrhh_id_nk')}}, ), migrations.CreateModel( - name="Convocatoria", + name='Convocatoria', fields=[ - ( - "id", - models.PositiveSmallIntegerField( - primary_key=True, serialize=False, verbose_name="año" - ), - ), - ("num_max_equipos", models.PositiveSmallIntegerField(default=4)), - ("fecha_min_solicitudes", models.DateField()), - ("fecha_max_solicitudes", models.DateField()), - ("fecha_max_aceptos", models.DateField()), - ("fecha_max_visto_buenos", models.DateField()), + ('id', models.PositiveSmallIntegerField(primary_key=True, serialize=False, verbose_name='año')), + ('num_max_equipos', models.PositiveSmallIntegerField(default=4)), + ('fecha_min_solicitudes', models.DateField()), + ('fecha_max_solicitudes', models.DateField()), + ('fecha_max_aceptos', models.DateField()), + ('fecha_max_visto_buenos', models.DateField()), ], ), migrations.CreateModel( - name="Departamento", + name='Departamento', fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ( - "academico_id_nk", - models.IntegerField( - blank=True, - db_index=True, - null=True, - verbose_name="cód. académico", - ), - ), + ('id', models.AutoField(primary_key=True, serialize=False)), ( - "rrhh_id_nk", - models.CharField( - blank=True, max_length=4, null=True, verbose_name="cód. RRHH" - ), + 'academico_id_nk', + models.IntegerField(blank=True, db_index=True, null=True, verbose_name='cód. académico'), ), - ("nombre", models.CharField(blank=True, max_length=255, null=True)), + ('rrhh_id_nk', models.CharField(blank=True, max_length=4, null=True, verbose_name='cód. RRHH')), + ('nombre', models.CharField(blank=True, max_length=255, null=True)), ( - "email", - models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="email del departamento", - ), + 'email', + models.EmailField(blank=True, max_length=254, null=True, verbose_name='email del departamento'), ), ( - "email_secretaria", - models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="email de la secretaría", - ), + 'email_secretaria', + models.EmailField(blank=True, max_length=254, null=True, verbose_name='email de la secretaría'), ), + ('nip_director', models.PositiveIntegerField(blank=True, null=True, verbose_name='NIP del director')), ( - "nip_director", - models.PositiveIntegerField( - blank=True, null=True, verbose_name="NIP del director" - ), + 'nombre_director', + models.CharField(blank=True, max_length=255, null=True, verbose_name='nombre del director'), ), ( - "nombre_director", - models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="nombre del director", - ), + 'email_director', + models.EmailField(blank=True, max_length=254, null=True, verbose_name='email del director'), ), ( - "email_director", - models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="email del director", - ), - ), - ( - "unidad_gasto", - models.CharField( - blank=True, - max_length=3, - null=True, - verbose_name="unidad de gasto", - ), + 'unidad_gasto', + models.CharField(blank=True, max_length=3, null=True, verbose_name='unidad de gasto'), ), ], - options={"unique_together": {("academico_id_nk", "rrhh_id_nk")}}, + options={'unique_together': {('academico_id_nk', 'rrhh_id_nk')}}, ), migrations.CreateModel( - name="Estudio", + name='Estudio', fields=[ ( - "id", - models.PositiveSmallIntegerField( - primary_key=True, serialize=False, verbose_name="Cód. estudio" - ), - ), - ("nombre", models.CharField(max_length=255)), - ( - "esta_activo", - models.BooleanField(default=True, verbose_name="¿Activo?"), + 'id', + models.PositiveSmallIntegerField(primary_key=True, serialize=False, verbose_name='Cód. estudio'), ), + ('nombre', models.CharField(max_length=255)), + ('esta_activo', models.BooleanField(default=True, verbose_name='¿Activo?')), ( - "rama", + 'rama', models.CharField( choices=[ - ("B", "Formación básica sin rama"), - ("H", "Artes y Humanidades"), - ("J", "Ciencias Sociales y Jurídicas"), - ("P", "Títulos Propios"), - ("S", "Ciencias de la Salud"), - ("T", "Ingeniería y Arquitectura"), - ("X", "Ciencias"), + ('B', 'Formación básica sin rama'), + ('H', 'Artes y Humanidades'), + ('J', 'Ciencias Sociales y Jurídicas'), + ('P', 'Títulos Propios'), + ('S', 'Ciencias de la Salud'), + ('T', 'Ingeniería y Arquitectura'), + ('X', 'Ciencias'), ], max_length=1, ), ), ], - options={"ordering": ["nombre"]}, + options={'ordering': ['nombre']}, ), migrations.CreateModel( - name="Evento", - fields=[ - ( - "nombre", - models.CharField(max_length=31, primary_key=True, serialize=False), - ) - ], + name='Evento', fields=[('nombre', models.CharField(max_length=31, primary_key=True, serialize=False))] ), migrations.CreateModel( - name="Licencia", + name='Licencia', fields=[ ( - "identificador", + 'identificador', models.CharField( - help_text="Ver los identificadores estándar en https://spdx.org/licenses/", + help_text='Ver los identificadores estándar en https://spdx.org/licenses/', max_length=255, primary_key=True, serialize=False, ), ), - ("nombre", models.CharField(max_length=255)), - ( - "url", - models.URLField( - blank=True, max_length=255, null=True, verbose_name="URL" - ), - ), + ('nombre', models.CharField(max_length=255)), + ('url', models.URLField(blank=True, max_length=255, null=True, verbose_name='URL')), ], ), migrations.CreateModel( - name="Linea", + name='Linea', fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("nombre", models.CharField(max_length=31)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nombre', models.CharField(max_length=31)), ], ), migrations.CreateModel( - name="Programa", + name='Programa', fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nombre_corto', models.CharField(help_text='Ejemplo: PRACUZ', max_length=15)), ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "nombre_corto", - models.CharField(help_text="Ejemplo: PRACUZ", max_length=15), - ), - ( - "nombre_largo", + 'nombre_largo', models.CharField( - help_text="Ejemplo: Programa de Recursos en Abierto para Centros", - max_length=127, + help_text='Ejemplo: Programa de Recursos en Abierto para Centros', max_length=127 ), ), ( - "max_ayuda", + 'max_ayuda', models.PositiveSmallIntegerField( - null=True, - verbose_name="Cuantía máxima que se puede solicitar de ayuda", + null=True, verbose_name='Cuantía máxima que se puede solicitar de ayuda' ), ), ( - "max_estudiantes", + 'max_estudiantes', models.PositiveSmallIntegerField( - null=True, - verbose_name="Número máximo de estudiantes por programa", + null=True, verbose_name='Número máximo de estudiantes por programa' ), ), - ("campos", models.TextField(null=True)), + ('campos', models.TextField(null=True)), ( - "requiere_visto_bueno", + 'requiere_visto_bueno', models.BooleanField( - default="False", - verbose_name="¿Requiere el visto bueno del director o decano?", + default='False', verbose_name='¿Requiere el visto bueno del director o decano?' ), ), ( - "convocatoria", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="indo.Convocatoria", - ), + 'convocatoria', + models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Convocatoria'), ), ], ), migrations.CreateModel( - name="Proyecto", + name='Proyecto', fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("codigo", models.CharField(max_length=31, null=True)), - ("titulo", models.CharField(max_length=255, verbose_name="Título")), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('codigo', models.CharField(max_length=31, null=True)), + ('titulo', models.CharField(max_length=255, verbose_name='Título')), ( - "descripcion", + 'descripcion', models.TextField( - help_text="Resumen sucinto del proyecto. Máximo recomendable: un párrafo de 10 líneas.", + help_text='Resumen sucinto del proyecto. Máximo recomendable: un párrafo de 10 líneas.', max_length=4095, null=True, - verbose_name="Descripción", + verbose_name='Descripción', ), ), ( - "estado", + 'estado', models.CharField( choices=[ - ("ANULADO", "Solicitud anulada"), - ("BORRADOR", "Solicitud en preparación"), - ("SOLICITADO", "Solicitud presentada"), + ('ANULADO', 'Solicitud anulada'), + ('BORRADOR', 'Solicitud en preparación'), + ('SOLICITADO', 'Solicitud presentada'), ], - default="BORRADOR", + default='BORRADOR', max_length=63, ), ), ( - "contexto", + 'contexto', models.TextField( blank=True, - help_text="Necesidad a la que responde el proyecto, mejoras esperadas respecto al estado de la cuestión, conocimiento que se genera.", + help_text='Necesidad a la que responde el proyecto, mejoras esperadas respecto al estado de la cuestión, conocimiento que se genera.', null=True, - verbose_name="Contexto del proyecto", - ), - ), - ( - "objetivos", - models.TextField( - blank=True, null=True, verbose_name="Objetivos del Proyecto" + verbose_name='Contexto del proyecto', ), ), + ('objetivos', models.TextField(blank=True, null=True, verbose_name='Objetivos del Proyecto')), ( - "metodos_estudio", + 'metodos_estudio', models.TextField( blank=True, - help_text="Métodos/técnicas utilizadas, características de la muestra, actividades previstas por los estudiantes y por el equipo del proyecto, calendario de actividades.", + help_text='Métodos/técnicas utilizadas, características de la muestra, actividades previstas por los estudiantes y por el equipo del proyecto, calendario de actividades.', null=True, - verbose_name="Métodos de estudio/experimentación y trabajo de campo", + verbose_name='Métodos de estudio/experimentación y trabajo de campo', ), ), ( - "mejoras", + 'mejoras', models.TextField( blank=True, - help_text="Método de evaluación, Resultados, Impacto (Eficiencia y Eficacia)", + help_text='Método de evaluación, Resultados, Impacto (Eficiencia y Eficacia)', null=True, - verbose_name="Mejoras esperadas en el proceso de enseñanza-aprendizaje y cómo se comprobarán.", + verbose_name='Mejoras esperadas en el proceso de enseñanza-aprendizaje y cómo se comprobarán.', ), ), ( - "continuidad", + 'continuidad', models.TextField( blank=True, - help_text="Transferibilidad, Sostenibilidad, Difusión prevista", + help_text='Transferibilidad, Sostenibilidad, Difusión prevista', null=True, - verbose_name="Continuidad y Expansión", + verbose_name='Continuidad y Expansión', ), ), ( - "tipo", + 'tipo', models.TextField( blank=True, - help_text="Experiencia, Estudio o Desarrollo", + help_text='Experiencia, Estudio o Desarrollo', null=True, - verbose_name="Tipo de proyecto", + verbose_name='Tipo de proyecto', ), ), ( - "contexto_aplicacion", + 'contexto_aplicacion', models.TextField( blank=True, - help_text="Centro, titulación, curso...", + help_text='Centro, titulación, curso...', null=True, - verbose_name="Contexto de aplicación/Público objetivo", + verbose_name='Contexto de aplicación/Público objetivo', ), ), ( - "metodos", - models.TextField( - blank=True, - null=True, - verbose_name="Métodos/Técnicas/Actividades utilizadas", - ), + 'metodos', + models.TextField(blank=True, null=True, verbose_name='Métodos/Técnicas/Actividades utilizadas'), ), + ('tecnologias', models.TextField(blank=True, null=True, verbose_name='Tecnologías utilizadas')), ( - "tecnologias", + 'aplicacion', models.TextField( - blank=True, null=True, verbose_name="Tecnologías utilizadas" + blank=True, null=True, verbose_name='Posible aplicación a otros centros/áreas de conocimiento' ), ), ( - "aplicacion", + 'proyectos_anteriores', models.TextField( blank=True, + help_text='Nombres de los proyectos de innovación realizados en cursos anteriores que estén relacionados con la temática propuesta.', null=True, - verbose_name="Posible aplicación a otros centros/áreas de conocimiento", + verbose_name='Proyectos anteriores', ), ), + ('impacto', models.TextField(blank=True, null=True, verbose_name='Impacto del proyecto')), + ('innovacion', models.TextField(blank=True, null=True, verbose_name='Tipo de innovación introducida')), ( - "proyectos_anteriores", + 'interes', models.TextField( blank=True, - help_text="Nombres de los proyectos de innovación realizados en cursos anteriores que estén relacionados con la temática propuesta.", null=True, - verbose_name="Proyectos anteriores", - ), - ), - ( - "impacto", - models.TextField( - blank=True, null=True, verbose_name="Impacto del proyecto" + verbose_name='Interés y oportunidad para la institución/titulación/centro', ), ), ( - "innovacion", + 'justificacion_equipo', models.TextField( blank=True, + help_text='Experiencia común conjunta, experiencia previa en el tipo de curso solicitado, etc.', null=True, - verbose_name="Tipo de innovación introducida", + verbose_name='Justificación del equipo docente que conforma la solicitud', ), ), ( - "interes", - models.TextField( - blank=True, - null=True, - verbose_name="Interés y oportunidad para la institución/titulación/centro", - ), + 'caracter_estrategico', + models.TextField(blank=True, null=True, verbose_name='Carácter estratégico del curso para la UZ'), ), ( - "justificacion_equipo", - models.TextField( - blank=True, - help_text="Experiencia común conjunta, experiencia previa en el tipo de curso solicitado, etc.", - null=True, - verbose_name="Justificación del equipo docente que conforma la solicitud", - ), + 'seminario', + models.TextField(blank=True, null=True, verbose_name='Asignatura, curso, seminario o equivalente'), ), + ('idioma', models.TextField(blank=True, null=True, verbose_name='Idioma de publicación')), + ('ramas', models.TextField(blank=True, null=True, verbose_name='Ramas de conocimiento')), ( - "caracter_estrategico", + 'mejoras_pou', models.TextField( blank=True, + help_text='Método de evaluación, Resultados, Impacto (Eficiencia y Eficacia)', null=True, - verbose_name="Carácter estratégico del curso para la UZ", + verbose_name='Mejoras esperadas en el Plan de Orientación Universitaria y cómo se comprobarán.', ), ), ( - "seminario", - models.TextField( - blank=True, - null=True, - verbose_name="Asignatura, curso, seminario o equivalente", - ), - ), - ( - "idioma", - models.TextField( - blank=True, null=True, verbose_name="Idioma de publicación" - ), - ), - ( - "ramas", - models.TextField( - blank=True, null=True, verbose_name="Ramas de conocimiento" - ), - ), - ( - "mejoras_pou", - models.TextField( - blank=True, - help_text="Método de evaluación, Resultados, Impacto (Eficiencia y Eficacia)", - null=True, - verbose_name="Mejoras esperadas en el Plan de Orientación Universitaria y cómo se comprobarán.", - ), - ), - ( - "ambito", + 'ambito', models.TextField( blank=True, help_text="Consultar las áreas en el bloque derecho de https://ocw.unizar.es/ocw/course/index.php?categoryid=8.", null=True, - verbose_name="Ámbito o ámbitos correspondientes a su área de conocimiento", + verbose_name='Ámbito o ámbitos correspondientes a su área de conocimiento', ), ), ( - "contenidos", + 'contenidos', models.TextField( blank=True, - help_text="Para OCW indicar los temas, que incluirán teoría, problemas, autoevaluación, etc.", + help_text='Para OCW indicar los temas, que incluirán teoría, problemas, autoevaluación, etc.', null=True, - verbose_name="Breve descripción de los contenidos", + verbose_name='Breve descripción de los contenidos', ), ), ( - "afectadas", - models.TextField( - blank=True, - null=True, - verbose_name="Asignatura/s y Titulación/es afectadas", - ), + 'afectadas', + models.TextField(blank=True, null=True, verbose_name='Asignatura/s y Titulación/es afectadas'), ), ( - "formatos", - models.TextField( - blank=True, - null=True, - verbose_name="Formatos de los materiales incluidos.", - ), + 'formatos', + models.TextField(blank=True, null=True, verbose_name='Formatos de los materiales incluidos.'), ), ( - "enlace", + 'enlace', models.TextField( blank=True, - help_text="Incluir el enlace (o enlaces) a la página de los estudios en la que se encuentra el plan de mejora y una mención expresa a qué aspecto del mismo se refiere el proyecto.", + help_text='Incluir el enlace (o enlaces) a la página de los estudios en la que se encuentra el plan de mejora y una mención expresa a qué aspecto del mismo se refiere el proyecto.', null=True, - verbose_name="Enlace", + verbose_name='Enlace', ), ), ( - "contenido_modulos", + 'contenido_modulos', models.TextField( blank=True, - help_text="Los cursos 0 deberán incluir un capítulo 0 con las competencias demandadas al alumnado que va a comenzar el estudio o estudios objeto del curso.", + help_text='Los cursos 0 deberán incluir un capítulo 0 con las competencias demandadas al alumnado que va a comenzar el estudio o estudios objeto del curso.', null=True, - verbose_name="Breve descripción de los contenidos de cada capítulo/módulo", + verbose_name='Breve descripción de los contenidos de cada capítulo/módulo', ), ), ( - "material_previo", + 'material_previo', models.TextField( - blank=True, - null=True, - verbose_name="Indicar si se cuenta con algún material previo", + blank=True, null=True, verbose_name='Indicar si se cuenta con algún material previo' ), ), ( - "duracion", + 'duracion', models.TextField( blank=True, - help_text="Número de semanas y número de horas de estudio y trabajo autónomo del participante en todo el curso.", + help_text='Número de semanas y número de horas de estudio y trabajo autónomo del participante en todo el curso.', null=True, - verbose_name="Duración del curso", + verbose_name='Duración del curso', ), ), ( - "multimedia", + 'multimedia', models.TextField( blank=True, - help_text="Elementos multimedia e innovadores que va a utilizar en la elaboración del curso.", + help_text='Elementos multimedia e innovadores que va a utilizar en la elaboración del curso.', null=True, - verbose_name="Elementos multimedia e innovadores", + verbose_name='Elementos multimedia e innovadores', ), ), ( - "indicadores", + 'indicadores', models.TextField( - blank=True, - null=True, - verbose_name="Indicadores para el seguimiento y evaluación del curso", + blank=True, null=True, verbose_name='Indicadores para el seguimiento y evaluación del curso' ), ), ( - "actividades", + 'actividades', models.TextField( blank=True, - help_text="Sólo obligatorias para MOOCs.", + help_text='Sólo obligatorias para MOOCs.', null=True, - verbose_name="Actividades de dinamización previstas", + verbose_name='Actividades de dinamización previstas', ), ), ( - "financiacion", + 'financiacion', models.TextField( blank=True, - help_text="Justificar la necesidad de lo solicitado. Añadir información sobre otras fuentes de financiación.", + help_text='Justificar la necesidad de lo solicitado. Añadir información sobre otras fuentes de financiación.', null=True, - verbose_name="Financiación", + verbose_name='Financiación', ), ), ( - "ayuda", + 'ayuda', models.PositiveIntegerField( blank=True, default=0, - help_text="Las normas de la convocatoria establecen el importe máximo que se puede solicitar según el programa.", + help_text='Las normas de la convocatoria establecen el importe máximo que se puede solicitar según el programa.', null=True, - verbose_name="Ayuda económica solicitada", + verbose_name='Ayuda económica solicitada', ), ), + ('visto_bueno', models.BooleanField(null=True, verbose_name='Visto bueno')), ( - "visto_bueno", - models.BooleanField(null=True, verbose_name="Visto bueno"), - ), - ( - "centro", + 'centro', models.ForeignKey( blank=True, - help_text="Sólo obligatorio para PIEC, PRACUZ, PIPOUZ.", + help_text='Sólo obligatorio para PIEC, PRACUZ, PIPOUZ.', null=True, on_delete=django.db.models.deletion.PROTECT, - related_name="proyectos", - to="indo.Centro", + related_name='proyectos', + to='indo.Centro', ), ), ( - "convocatoria", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="indo.Convocatoria", - ), + 'convocatoria', + models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Convocatoria'), ), ( - "departamento", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.PROTECT, - to="indo.Departamento", - ), + 'departamento', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='indo.Departamento'), ), ( - "estudio", + 'estudio', models.ForeignKey( blank=True, - help_text="Sólo obligatorio para PIET.", - limit_choices_to={"esta_activo": True}, + help_text='Sólo obligatorio para PIET.', + limit_choices_to={'esta_activo': True}, null=True, on_delete=django.db.models.deletion.PROTECT, - to="indo.Estudio", + to='indo.Estudio', ), ), ( - "licencia", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.PROTECT, - to="indo.Licencia", - ), + 'licencia', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='indo.Licencia'), ), ( - "linea", + 'linea', models.ForeignKey( blank=True, - help_text="En su caso.", + help_text='En su caso.', null=True, on_delete=django.db.models.deletion.PROTECT, - to="indo.Linea", - verbose_name="Línea", - ), - ), - ( - "programa", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, to="indo.Programa" + to='indo.Linea', + verbose_name='Línea', ), ), + ('programa', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Programa')), ], ), migrations.CreateModel( - name="TipoEstudio", + name='TipoEstudio', fields=[ ( - "id", + 'id', models.PositiveSmallIntegerField( - primary_key=True, - serialize=False, - verbose_name="Cód. tipo estudio", + primary_key=True, serialize=False, verbose_name='Cód. tipo estudio' ), ), - ("nombre", models.CharField(max_length=63)), + ('nombre', models.CharField(max_length=63)), ], ), migrations.CreateModel( - name="TipoParticipacion", - fields=[ - ( - "nombre", - models.CharField(max_length=63, primary_key=True, serialize=False), - ) - ], + name='TipoParticipacion', + fields=[('nombre', models.CharField(max_length=63, primary_key=True, serialize=False))], ), migrations.CreateModel( - name="Registro", + name='Registro', fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("fecha", models.DateTimeField(auto_now_add=True)), - ("descripcion", models.CharField(max_length=255)), - ( - "evento", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, to="indo.Evento" - ), - ), - ( - "proyecto", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, to="indo.Proyecto" - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('fecha', models.DateTimeField(auto_now_add=True)), + ('descripcion', models.CharField(max_length=255)), + ('evento', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Evento')), + ('proyecto', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Proyecto')), ], ), migrations.CreateModel( - name="Plan", + name='Plan', fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id_nk', models.PositiveSmallIntegerField(verbose_name='Cód. plan')), ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), + 'nip_coordinador', + models.PositiveIntegerField(blank=True, null=True, verbose_name='NIP del coordinador'), ), - ("id_nk", models.PositiveSmallIntegerField(verbose_name="Cód. plan")), ( - "nip_coordinador", - models.PositiveIntegerField( - blank=True, null=True, verbose_name="NIP del coordinador" - ), + 'nombre_coordinador', + models.CharField(blank=True, max_length=255, null=True, verbose_name='nombre del coordinador'), ), ( - "nombre_coordinador", - models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="nombre del coordinador", - ), - ), - ( - "email_coordinador", - models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="email del coordinador", - ), - ), - ( - "esta_activo", - models.BooleanField(default=True, verbose_name="¿Activo?"), - ), - ( - "centro", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, to="indo.Centro" - ), - ), - ( - "estudio", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, to="indo.Estudio" - ), + 'email_coordinador', + models.EmailField(blank=True, max_length=254, null=True, verbose_name='email del coordinador'), ), + ('esta_activo', models.BooleanField(default=True, verbose_name='¿Activo?')), + ('centro', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Centro')), + ('estudio', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.Estudio')), ], ), migrations.CreateModel( - name="ParticipanteProyecto", + name='ParticipanteProyecto', fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "proyecto", + 'proyecto', models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="participantes", - to="indo.Proyecto", + on_delete=django.db.models.deletion.PROTECT, related_name='participantes', to='indo.Proyecto' ), ), ( - "tipo_participacion", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="indo.TipoParticipacion", - ), + 'tipo_participacion', + models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.TipoParticipacion'), ), ( - "usuario", + 'usuario', models.ForeignKey( on_delete=django.db.models.deletion.PROTECT, - related_name="vinculaciones", + related_name='vinculaciones', to=settings.AUTH_USER_MODEL, ), ), ], ), migrations.AddField( - model_name="linea", - name="programa", + model_name='linea', + name='programa', field=models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="lineas", - to="indo.Programa", + on_delete=django.db.models.deletion.PROTECT, related_name='lineas', to='indo.Programa' ), ), migrations.AddField( - model_name="estudio", - name="tipo_estudio", - field=models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, to="indo.TipoEstudio" - ), + model_name='estudio', + name='tipo_estudio', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='indo.TipoEstudio'), ), ] diff --git a/indo/migrations/0002_auto_20200131_1244.py b/indo/migrations/0002_auto_20200131_1244.py index 22735b7..c85dec5 100644 --- a/indo/migrations/0002_auto_20200131_1244.py +++ b/indo/migrations/0002_auto_20200131_1244.py @@ -6,15 +6,15 @@ import django.db.models.deletion def geo_post_migrate_signal(apps, schema_editor): - """Emit the post-migrate signal during the migration. + '''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") + ''' + indo_config = django_apps.get_app_config('indo') models.signals.post_migrate.send( sender=indo_config, app_config=indo_config, @@ -26,37 +26,28 @@ def geo_post_migrate_signal(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("indo", "0001_initial")] + dependencies = [('indo', '0001_initial')] operations = [ migrations.AlterModelOptions( - name="proyecto", - options={ - "permissions": [ - ("listar_proyectos", "Puede ver el listado de todos los proyectos.") - ] - }, + name='proyecto', + options={'permissions': [('listar_proyectos', 'Puede ver el listado de todos los proyectos.')]}, ), migrations.AlterField( - model_name="plan", - name="estudio", + model_name='plan', + name='estudio', field=models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="planes", - to="indo.Estudio", + on_delete=django.db.models.deletion.PROTECT, related_name='planes', to='indo.Estudio' ), ), migrations.AlterField( - model_name="proyecto", - name="descripcion", + model_name='proyecto', + name='descripcion', field=models.TextField( - help_text=( - "Resumen sucinto del proyecto. " - "Máximo recomendable: un párrafo de 10 líneas." - ), + help_text=('Resumen sucinto del proyecto. ' 'Máximo recomendable: un párrafo de 10 líneas.'), max_length=4095, null=True, - verbose_name="Resumen", + verbose_name='Resumen', ), ), migrations.RunPython(geo_post_migrate_signal), diff --git a/indo/migrations/0003_auto_20200131_1245.py b/indo/migrations/0003_auto_20200131_1245.py index 2f7ae45..739f3f4 100644 --- a/indo/migrations/0003_auto_20200131_1245.py +++ b/indo/migrations/0003_auto_20200131_1245.py @@ -6,26 +6,26 @@ from django.db import migrations, models def add_managers_group(apps, schema_editor): - Group = apps.get_model("auth", "Group") - Permission = apps.get_model("auth", "Permission") + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') - group, created = Group.objects.get_or_create(name="Gestores") + group, created = Group.objects.get_or_create(name='Gestores') if created: - print("Creado el grupo «Gestores».") - listar_proyectos = Permission.objects.get(codename="listar_proyectos") + print('Creado el grupo «Gestores».') + listar_proyectos = Permission.objects.get(codename='listar_proyectos') group.permissions.add(listar_proyectos) def geo_post_migrate_signal(apps, schema_editor): - """Emit the post-migrate signal during the migration. + '''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") + ''' + indo_config = django_apps.get_app_config('indo') models.signals.post_migrate.send( sender=indo_config, app_config=indo_config, @@ -37,9 +37,6 @@ def geo_post_migrate_signal(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("indo", "0002_auto_20200131_1244")] + dependencies = [('indo', '0002_auto_20200131_1244')] - operations = [ - migrations.RunPython(add_managers_group), - migrations.RunPython(geo_post_migrate_signal), - ] + operations = [migrations.RunPython(add_managers_group), migrations.RunPython(geo_post_migrate_signal)] diff --git a/indo/migrations/0004_auto_20200131_1409.py b/indo/migrations/0004_auto_20200131_1409.py index dc61157..857d4fb 100644 --- a/indo/migrations/0004_auto_20200131_1409.py +++ b/indo/migrations/0004_auto_20200131_1409.py @@ -5,24 +5,24 @@ from django.db import migrations, models def add_permission_to_group(apps, schema_editor): - group = apps.get_model("auth", "Group") - permission = apps.get_model("auth", "Permission") + group = apps.get_model('auth', 'Group') + permission = apps.get_model('auth', 'Permission') - gestores = group.objects.get(name="Gestores") - ver_proyecto = permission.objects.get(codename="ver_proyecto") + gestores = group.objects.get(name='Gestores') + ver_proyecto = permission.objects.get(codename='ver_proyecto') gestores.permissions.add(ver_proyecto) def geo_post_migrate_signal(apps, schema_editor): - """Emit the post-migrate signal during the migration. + '''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 later in this migration. - """ - indo_config = django_apps.get_app_config("indo") + ''' + indo_config = django_apps.get_app_config('indo') models.signals.post_migrate.send( sender=indo_config, app_config=indo_config, @@ -34,18 +34,15 @@ def geo_post_migrate_signal(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("indo", "0003_auto_20200131_1245")] + dependencies = [('indo', '0003_auto_20200131_1245')] operations = [ migrations.AlterModelOptions( - name="proyecto", + name='proyecto', options={ - "permissions": [ - ( - "listar_proyectos", - "Puede ver el listado de todos los proyectos presentados.", - ), - ("ver_proyecto", "Puede ver cualquier proyecto."), + 'permissions': [ + ('listar_proyectos', 'Puede ver el listado de todos los proyectos presentados.'), + ('ver_proyecto', 'Puede ver cualquier proyecto.'), ] }, ), diff --git a/indo/migrations/0005_auto_20200203_1316.py b/indo/migrations/0005_auto_20200203_1316.py index 00a1629..1862da5 100644 --- a/indo/migrations/0005_auto_20200203_1316.py +++ b/indo/migrations/0005_auto_20200203_1316.py @@ -5,45 +5,37 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [("indo", "0004_auto_20200131_1409")] + dependencies = [('indo', '0004_auto_20200131_1409')] operations = [ migrations.AlterModelOptions( - name="proyecto", + name='proyecto', options={ - "permissions": [ - ( - "listar_proyectos", - "Puede ver el listado de todos los proyectos.", - ), - ("ver_proyecto", "Puede ver cualquier proyecto."), + 'permissions': [ + ('listar_proyectos', 'Puede ver el listado de todos los proyectos.'), + ('ver_proyecto', 'Puede ver cualquier proyecto.'), ] }, ), migrations.RenameField( - model_name="programa", - old_name="requiere_visto_bueno", - new_name="requiere_visto_bueno_centro", + model_name='programa', old_name='requiere_visto_bueno', new_name='requiere_visto_bueno_centro' ), - migrations.RemoveField(model_name="proyecto", name="visto_bueno"), + migrations.RemoveField(model_name='proyecto', name='visto_bueno'), migrations.AddField( - model_name="programa", - name="requiere_visto_bueno_estudio", + model_name='programa', + name='requiere_visto_bueno_estudio', field=models.BooleanField( - default="False", - verbose_name="¿Requiere el visto bueno del coordinador del plan de estudios?", + default='False', verbose_name='¿Requiere el visto bueno del coordinador del plan de estudios?' ), ), migrations.AddField( - model_name="proyecto", - name="visto_bueno_centro", - field=models.BooleanField(null=True, verbose_name="Visto bueno del centro"), + model_name='proyecto', + name='visto_bueno_centro', + field=models.BooleanField(null=True, verbose_name='Visto bueno del centro'), ), migrations.AddField( - model_name="proyecto", - name="visto_bueno_estudio", - field=models.BooleanField( - null=True, verbose_name="Visto bueno del plan de estudios" - ), + model_name='proyecto', + name='visto_bueno_estudio', + field=models.BooleanField(null=True, verbose_name='Visto bueno del plan de estudios'), ), ] diff --git a/indo/migrations/0006_auto_20200207_0919.py b/indo/migrations/0006_auto_20200207_0919.py index bc093e6..8263565 100644 --- a/indo/migrations/0006_auto_20200207_0919.py +++ b/indo/migrations/0006_auto_20200207_0919.py @@ -5,24 +5,24 @@ from django.db import migrations, models def add_permission_to_group(apps, schema_editor): - group = apps.get_model("auth", "Group") - permission = apps.get_model("auth", "Permission") + group = apps.get_model('auth', 'Group') + permission = apps.get_model('auth', 'Permission') - gestores = group.objects.get(name="Gestores") - ver_proyecto = permission.objects.get(codename="editar_proyecto") + gestores = group.objects.get(name='Gestores') + ver_proyecto = permission.objects.get(codename='editar_proyecto') gestores.permissions.add(ver_proyecto) def geo_post_migrate_signal(apps, schema_editor): - """Emit the post-migrate signal during the migration. + '''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 later in this migration. - """ - indo_config = django_apps.get_app_config("indo") + ''' + indo_config = django_apps.get_app_config('indo') models.signals.post_migrate.send( sender=indo_config, app_config=indo_config, @@ -34,22 +34,16 @@ def geo_post_migrate_signal(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [("indo", "0005_auto_20200203_1316")] + dependencies = [('indo', '0005_auto_20200203_1316')] operations = [ migrations.AlterModelOptions( - name="proyecto", + name='proyecto', options={ - "permissions": [ - ( - "listar_proyectos", - "Puede ver el listado de todos los proyectos.", - ), - ("ver_proyecto", "Puede ver cualquier proyecto."), - ( - "editar_proyecto", - "Puede editar cualquier proyecto en cualquier momento.", - ), + 'permissions': [ + ('listar_proyectos', 'Puede ver el listado de todos los proyectos.'), + ('ver_proyecto', 'Puede ver cualquier proyecto.'), + ('editar_proyecto', 'Puede editar cualquier proyecto en cualquier momento.'), ] }, ), diff --git a/indo/models.py b/indo/models.py index 0948525..2895236 100644 --- a/indo/models.py +++ b/indo/models.py @@ -5,66 +5,42 @@ from django.utils.translation import gettext_lazy as _ class Centro(models.Model): id = models.AutoField(primary_key=True) - academico_id_nk = models.IntegerField(_("cód. académico"), blank=True, null=True) - rrhh_id_nk = models.CharField(_("cód. RRHH"), max_length=4, blank=True, null=True) + academico_id_nk = models.IntegerField(_('cód. académico'), blank=True, null=True) + rrhh_id_nk = models.CharField(_('cód. RRHH'), max_length=4, blank=True, null=True) nombre = models.CharField(max_length=255) - tipo_centro = models.CharField( - _("tipo de centro"), max_length=30, blank=True, null=True - ) - direccion = models.CharField(_("dirección"), max_length=140, blank=True, null=True) + tipo_centro = models.CharField(_('tipo de centro'), max_length=30, blank=True, null=True) + direccion = models.CharField(_('dirección'), max_length=140, blank=True, null=True) municipio = models.CharField(max_length=100, blank=True, null=True) - telefono = models.CharField(_("teléfono"), max_length=30, blank=True, null=True) - email = models.EmailField(_("email address"), blank=True, null=True) - url = models.URLField("URL", max_length=255, blank=True, null=True) - nip_decano = models.PositiveIntegerField( - _("NIP del decano o director"), blank=True, null=True - ) - nombre_decano = models.CharField( - _("nombre del decano o director"), max_length=255, blank=True, null=True - ) - email_decano = models.EmailField( - _("email del decano o director"), blank=True, null=True - ) + telefono = models.CharField(_('teléfono'), max_length=30, blank=True, null=True) + email = models.EmailField(_('email address'), blank=True, null=True) + url = models.URLField('URL', max_length=255, blank=True, null=True) + nip_decano = models.PositiveIntegerField(_('NIP del decano o director'), blank=True, null=True) + nombre_decano = models.CharField(_('nombre del decano o director'), max_length=255, blank=True, null=True) + email_decano = models.EmailField(_('email del decano o director'), blank=True, null=True) tratamiento_decano = models.CharField( - _("cargo"), - max_length=25, - blank=True, - null=True, - help_text=_("Decano/a ó director(a)."), - ) - nip_secretario = models.PositiveIntegerField( - _("NIP del secretario"), blank=True, null=True - ) - nombre_secretario = models.CharField( - _("nombre del secretario"), max_length=255, blank=True, null=True - ) - email_secretario = models.EmailField( - _("email del secretario"), blank=True, null=True - ) - nips_coord_pou = models.CharField( - _("NIPs de los coordinadores POU"), blank=True, max_length=255, null=True + _('cargo'), max_length=25, blank=True, null=True, help_text=_('Decano/a ó director(a).') ) + nip_secretario = models.PositiveIntegerField(_('NIP del secretario'), blank=True, null=True) + nombre_secretario = models.CharField(_('nombre del secretario'), max_length=255, blank=True, null=True) + email_secretario = models.EmailField(_('email del secretario'), blank=True, null=True) + nips_coord_pou = models.CharField(_('NIPs de los coordinadores POU'), blank=True, max_length=255, null=True) nombres_coords_pou = models.CharField( - _("nombres de los coordinadores POU"), blank=True, max_length=1023, null=True - ) - emails_coords_pou = models.CharField( - _("emails de los coordinadores POU"), blank=True, max_length=1023, null=True + _('nombres de los coordinadores POU'), blank=True, max_length=1023, null=True ) - unidad_gasto = models.CharField( - _("unidad de gasto"), blank=True, max_length=3, null=True - ) - esta_activo = models.BooleanField(_("¿Activo?"), default=False) + emails_coords_pou = models.CharField(_('emails de los coordinadores POU'), blank=True, max_length=1023, null=True) + unidad_gasto = models.CharField(_('unidad de gasto'), blank=True, max_length=3, null=True) + esta_activo = models.BooleanField(_('¿Activo?'), default=False) class Meta: - unique_together = ["academico_id_nk", "rrhh_id_nk"] - ordering = ["nombre"] + 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})" + return f'{self.nombre} ({self.academico_id_nk} / {self.rrhh_id_nk})' class Convocatoria(models.Model): - id = models.PositiveSmallIntegerField(_("año"), primary_key=True) + id = models.PositiveSmallIntegerField(_('año'), primary_key=True) num_max_equipos = models.PositiveSmallIntegerField(default=4) fecha_min_solicitudes = models.DateField() fecha_max_solicitudes = models.DateField() @@ -77,54 +53,44 @@ class Convocatoria(models.Model): class Departamento(models.Model): id = models.AutoField(primary_key=True) - academico_id_nk = models.IntegerField( - "cód. académico", blank=True, db_index=True, null=True - ) - rrhh_id_nk = models.CharField("cód. RRHH", max_length=4, blank=True, null=True) + academico_id_nk = models.IntegerField('cód. académico', blank=True, db_index=True, null=True) + rrhh_id_nk = models.CharField('cód. RRHH', max_length=4, blank=True, null=True) nombre = models.CharField(max_length=255, blank=True, null=True) - email = models.EmailField(_("email del departamento"), blank=True, null=True) - email_secretaria = models.EmailField( - _("email de la secretaría"), blank=True, null=True - ) - nip_director = models.PositiveIntegerField( - _("NIP del director"), blank=True, null=True - ) - nombre_director = models.CharField( - _("nombre del director"), max_length=255, blank=True, null=True - ) - email_director = models.EmailField(_("email del director"), blank=True, null=True) - unidad_gasto = models.CharField( - _("unidad de gasto"), blank=True, max_length=3, null=True - ) + email = models.EmailField(_('email del departamento'), blank=True, null=True) + email_secretaria = models.EmailField(_('email de la secretaría'), blank=True, null=True) + nip_director = models.PositiveIntegerField(_('NIP del director'), blank=True, null=True) + nombre_director = models.CharField(_('nombre del director'), max_length=255, blank=True, null=True) + email_director = models.EmailField(_('email del director'), blank=True, null=True) + unidad_gasto = models.CharField(_('unidad de gasto'), blank=True, max_length=3, null=True) class Meta: - unique_together = ["academico_id_nk", "rrhh_id_nk"] + unique_together = ['academico_id_nk', 'rrhh_id_nk'] def __str__(self): - return f"{self.nombre} ({self.academico_id_nk} / {self.rrhh_id_nk})" + return f'{self.nombre} ({self.academico_id_nk} / {self.rrhh_id_nk})' class Estudio(models.Model): OPCIONES_RAMA = ( - ("B", "Formación básica sin rama"), - ("H", "Artes y Humanidades"), - ("J", "Ciencias Sociales y Jurídicas"), - ("P", "Títulos Propios"), - ("S", "Ciencias de la Salud"), - ("T", "Ingeniería y Arquitectura"), - ("X", "Ciencias"), - ) - id = models.PositiveSmallIntegerField(_("Cód. estudio"), primary_key=True) + ('B', 'Formación básica sin rama'), + ('H', 'Artes y Humanidades'), + ('J', 'Ciencias Sociales y Jurídicas'), + ('P', 'Títulos Propios'), + ('S', 'Ciencias de la Salud'), + ('T', 'Ingeniería y Arquitectura'), + ('X', 'Ciencias'), + ) + id = models.PositiveSmallIntegerField(_('Cód. estudio'), primary_key=True) nombre = models.CharField(max_length=255) - esta_activo = models.BooleanField(_("¿Activo?"), default=True) + esta_activo = models.BooleanField(_('¿Activo?'), default=True) rama = models.CharField(max_length=1, choices=OPCIONES_RAMA) - tipo_estudio = models.ForeignKey("TipoEstudio", on_delete=models.PROTECT) + tipo_estudio = models.ForeignKey('TipoEstudio', on_delete=models.PROTECT) class Meta: - ordering = ["nombre"] + ordering = ['nombre'] def __str__(self): - return f"{self.nombre} ({self.tipo_estudio.nombre})" + return f'{self.nombre} ({self.tipo_estudio.nombre})' class Evento(models.Model): @@ -132,417 +98,322 @@ class Evento(models.Model): class Licencia(models.Model): - """Licencia de publicación de la memoria""" + '''Licencia de publicación de la memoria''' identificador = models.CharField( - max_length=255, - primary_key=True, - help_text=_("Ver los identificadores estándar en https://spdx.org/licenses/"), + max_length=255, primary_key=True, help_text=_('Ver los identificadores estándar en https://spdx.org/licenses/') ) nombre = models.CharField(max_length=255) - url = models.URLField("URL", max_length=255, blank=True, null=True) + url = models.URLField('URL', max_length=255, blank=True, null=True) def __str__(self): - return f"{self.nombre} ({self.identificador})" + return f'{self.nombre} ({self.identificador})' class Linea(models.Model): nombre = models.CharField(max_length=31) - programa = models.ForeignKey( - "Programa", on_delete=models.PROTECT, related_name="lineas" - ) + programa = models.ForeignKey('Programa', on_delete=models.PROTECT, related_name='lineas') def __str__(self): - return f"{self.nombre}" + return f'{self.nombre}' class Plan(models.Model): - id_nk = models.PositiveSmallIntegerField(_("Cód. plan")) - nip_coordinador = models.PositiveIntegerField( - _("NIP del coordinador"), blank=True, null=True - ) - nombre_coordinador = models.CharField( - _("nombre del coordinador"), max_length=255, blank=True, null=True - ) - email_coordinador = models.EmailField( - _("email del coordinador"), blank=True, null=True - ) - esta_activo = models.BooleanField(_("¿Activo?"), default=True) - centro = models.ForeignKey("Centro", on_delete=models.PROTECT) - estudio = models.ForeignKey( - "Estudio", on_delete=models.PROTECT, related_name="planes" - ) + id_nk = models.PositiveSmallIntegerField(_('Cód. plan')) + nip_coordinador = models.PositiveIntegerField(_('NIP del coordinador'), blank=True, null=True) + nombre_coordinador = models.CharField(_('nombre del coordinador'), max_length=255, blank=True, null=True) + email_coordinador = models.EmailField(_('email del coordinador'), blank=True, null=True) + esta_activo = models.BooleanField(_('¿Activo?'), default=True) + centro = models.ForeignKey('Centro', on_delete=models.PROTECT) + estudio = models.ForeignKey('Estudio', on_delete=models.PROTECT, related_name='planes') class ParticipanteProyecto(models.Model): - proyecto = models.ForeignKey( - "Proyecto", on_delete=models.PROTECT, related_name="participantes" - ) - tipo_participacion = models.ForeignKey( - "TipoParticipacion", on_delete=models.PROTECT - ) - usuario = models.ForeignKey( - "accounts.CustomUser", on_delete=models.PROTECT, related_name="vinculaciones" - ) + proyecto = models.ForeignKey('Proyecto', on_delete=models.PROTECT, related_name='participantes') + tipo_participacion = models.ForeignKey('TipoParticipacion', on_delete=models.PROTECT) + usuario = models.ForeignKey('accounts.CustomUser', on_delete=models.PROTECT, related_name='vinculaciones') def get_cargo(self): - if self.tipo_participacion.nombre == "coordinador": - return _("Coordinadora") if self.usuario.sexo == "F" else _("Coordinador") - if self.tipo_participacion.nombre == "coordinador_2": - if self.usuario.sexo == "F": - return _("Coordinadora auxiliar") - return _("Coordinador auxiliar") - return _("Participante") + if self.tipo_participacion.nombre == 'coordinador': + return _('Coordinadora') if self.usuario.sexo == 'F' else _('Coordinador') + if self.tipo_participacion.nombre == 'coordinador_2': + if self.usuario.sexo == 'F': + return _('Coordinadora auxiliar') + return _('Coordinador auxiliar') + return _('Participante') class Programa(models.Model): - nombre_corto = models.CharField(max_length=15, help_text=_("Ejemplo: PRACUZ")) + nombre_corto = models.CharField(max_length=15, help_text=_('Ejemplo: PRACUZ')) nombre_largo = models.CharField( - max_length=127, - help_text=_("Ejemplo: Programa de Recursos en Abierto para Centros"), - ) - max_ayuda = models.PositiveSmallIntegerField( - _("Cuantía máxima que se puede solicitar de ayuda"), null=True - ) - max_estudiantes = models.PositiveSmallIntegerField( - _("Número máximo de estudiantes por programa"), null=True + max_length=127, help_text=_('Ejemplo: Programa de Recursos en Abierto para Centros') ) + max_ayuda = models.PositiveSmallIntegerField(_('Cuantía máxima que se puede solicitar de ayuda'), null=True) + max_estudiantes = models.PositiveSmallIntegerField(_('Número máximo de estudiantes por programa'), null=True) campos = models.TextField(null=True) - convocatoria = models.ForeignKey("Convocatoria", on_delete=models.PROTECT) + convocatoria = models.ForeignKey('Convocatoria', on_delete=models.PROTECT) requiere_visto_bueno_centro = models.BooleanField( - _("¿Requiere el visto bueno del director o decano?"), default="False" + _('¿Requiere el visto bueno del director o decano?'), default='False' ) requiere_visto_bueno_estudio = models.BooleanField( - _("¿Requiere el visto bueno del coordinador del plan de estudios?"), - default="False", + _('¿Requiere el visto bueno del coordinador del plan de estudios?'), default='False' ) def __str__(self): - return f"{self.nombre_corto}" + return f'{self.nombre_corto}' class Proyecto(models.Model): id = models.AutoField(primary_key=True) codigo = models.CharField(max_length=31, null=True) - titulo = models.CharField(_("Título"), max_length=255) + titulo = models.CharField(_('Título'), max_length=255) descripcion = models.TextField( - _("Resumen"), + _('Resumen'), null=True, max_length=4095, - help_text=_( - "Resumen sucinto del proyecto. Máximo recomendable: " - "un párrafo de 10 líneas." - ), + help_text=_('Resumen sucinto del proyecto. Máximo recomendable: ' 'un párrafo de 10 líneas.'), ) estado = models.CharField( choices=( - ("ANULADO", "Solicitud anulada"), - ("BORRADOR", "Solicitud en preparación"), - ("SOLICITADO", "Solicitud presentada"), + ('ANULADO', 'Solicitud anulada'), + ('BORRADOR', 'Solicitud en preparación'), + ('SOLICITADO', 'Solicitud presentada'), ), - default="BORRADOR", + default='BORRADOR', max_length=63, ) contexto = models.TextField( - _("Contexto del proyecto"), + _('Contexto del proyecto'), blank=True, null=True, help_text=_( - "Necesidad a la que responde el proyecto, mejoras esperadas respecto " - "al estado de la cuestión, conocimiento que se genera." + 'Necesidad a la que responde el proyecto, mejoras esperadas respecto ' + 'al estado de la cuestión, conocimiento que se genera.' ), ) - objetivos = models.TextField(_("Objetivos del Proyecto"), blank=True, null=True) + objetivos = models.TextField(_('Objetivos del Proyecto'), blank=True, null=True) metodos_estudio = models.TextField( - _("Métodos de estudio/experimentación y trabajo de campo"), + _('Métodos de estudio/experimentación y trabajo de campo'), blank=True, null=True, help_text=_( - "Métodos/técnicas utilizadas, características de la muestra, " - "actividades previstas por los estudiantes y por el equipo del proyecto, " - "calendario de actividades." + 'Métodos/técnicas utilizadas, características de la muestra, ' + 'actividades previstas por los estudiantes y por el equipo del proyecto, ' + 'calendario de actividades.' ), ) mejoras = models.TextField( - _( - "Mejoras esperadas en el proceso de enseñanza-aprendizaje " - "y cómo se comprobarán." - ), + _('Mejoras esperadas en el proceso de enseñanza-aprendizaje ' 'y cómo se comprobarán.'), blank=True, null=True, - help_text=_( - "Método de evaluación, Resultados, Impacto (Eficiencia y Eficacia)" - ), + help_text=_('Método de evaluación, Resultados, Impacto (Eficiencia y Eficacia)'), ) continuidad = models.TextField( - _("Continuidad y Expansión"), + _('Continuidad y Expansión'), blank=True, null=True, - help_text=_("Transferibilidad, Sostenibilidad, Difusión prevista"), + help_text=_('Transferibilidad, Sostenibilidad, Difusión prevista'), ) tipo = models.TextField( - _("Tipo de proyecto"), - blank=True, - null=True, - help_text=_("Experiencia, Estudio o Desarrollo"), + _('Tipo de proyecto'), blank=True, null=True, help_text=_('Experiencia, Estudio o Desarrollo') ) contexto_aplicacion = models.TextField( - _("Contexto de aplicación/Público objetivo"), - blank=True, - null=True, - help_text=_("Centro, titulación, curso..."), - ) - metodos = models.TextField( - _("Métodos/Técnicas/Actividades utilizadas"), blank=True, null=True - ) - tecnologias = models.TextField(_("Tecnologías utilizadas"), blank=True, null=True) - aplicacion = models.TextField( - _("Posible aplicación a otros centros/áreas de conocimiento"), + _('Contexto de aplicación/Público objetivo'), blank=True, null=True, + help_text=_('Centro, titulación, curso...'), ) + metodos = models.TextField(_('Métodos/Técnicas/Actividades utilizadas'), blank=True, null=True) + tecnologias = models.TextField(_('Tecnologías utilizadas'), blank=True, null=True) + aplicacion = models.TextField(_('Posible aplicación a otros centros/áreas de conocimiento'), blank=True, null=True) proyectos_anteriores = models.TextField( - _("Proyectos anteriores"), + _('Proyectos anteriores'), blank=True, null=True, help_text=_( - "Nombres de los proyectos de innovación realizados en cursos anteriores " - "que estén relacionados con la temática propuesta." + 'Nombres de los proyectos de innovación realizados en cursos anteriores ' + 'que estén relacionados con la temática propuesta.' ), ) - impacto = models.TextField(_("Impacto del proyecto"), blank=True, null=True) - innovacion = models.TextField( - _("Tipo de innovación introducida"), blank=True, null=True - ) - interes = models.TextField( - _("Interés y oportunidad para la institución/titulación/centro"), - blank=True, - null=True, - ) + impacto = models.TextField(_('Impacto del proyecto'), blank=True, null=True) + innovacion = models.TextField(_('Tipo de innovación introducida'), blank=True, null=True) + interes = models.TextField(_('Interés y oportunidad para la institución/titulación/centro'), blank=True, null=True) justificacion_equipo = models.TextField( - _("Justificación del equipo docente que conforma la solicitud"), + _('Justificación del equipo docente que conforma la solicitud'), blank=True, null=True, - help_text=_( - "Experiencia común conjunta, experiencia previa en el tipo de curso " - "solicitado, etc." - ), - ) - caracter_estrategico = models.TextField( - _("Carácter estratégico del curso para la UZ"), blank=True, null=True + help_text=_('Experiencia común conjunta, experiencia previa en el tipo de curso ' 'solicitado, etc.'), ) - seminario = models.TextField( - _("Asignatura, curso, seminario o equivalente"), blank=True, null=True - ) - idioma = models.TextField(_("Idioma de publicación"), blank=True, null=True) - ramas = models.TextField(_("Ramas de conocimiento"), blank=True, null=True) + caracter_estrategico = models.TextField(_('Carácter estratégico del curso para la UZ'), blank=True, null=True) + seminario = models.TextField(_('Asignatura, curso, seminario o equivalente'), blank=True, null=True) + idioma = models.TextField(_('Idioma de publicación'), blank=True, null=True) + ramas = models.TextField(_('Ramas de conocimiento'), blank=True, null=True) mejoras_pou = models.TextField( - _( - "Mejoras esperadas en el Plan de Orientación Universitaria " - "y cómo se comprobarán." - ), + _('Mejoras esperadas en el Plan de Orientación Universitaria ' 'y cómo se comprobarán.'), blank=True, null=True, - help_text=_( - "Método de evaluación, Resultados, Impacto (Eficiencia y Eficacia)" - ), + help_text=_('Método de evaluación, Resultados, Impacto (Eficiencia y Eficacia)'), ) ambito = models.TextField( - _("Ámbito o ámbitos correspondientes a su área de conocimiento"), + _('Ámbito o ámbitos correspondientes a su área de conocimiento'), blank=True, null=True, help_text=_( - "Consultar las áreas en el bloque derecho de " + 'Consultar las áreas en el bloque derecho de ' "https://ocw.unizar.es/ocw/course/index.php?categoryid=8" - "." + '.' ), ) contenidos = models.TextField( - _("Breve descripción de los contenidos"), + _('Breve descripción de los contenidos'), blank=True, null=True, - help_text=_( - "Para OCW indicar los temas, que incluirán teoría, problemas, " - "autoevaluación, etc." - ), - ) - afectadas = models.TextField( - _("Asignatura/s y Titulación/es afectadas"), blank=True, null=True - ) - formatos = models.TextField( - _("Formatos de los materiales incluidos."), blank=True, null=True + help_text=_('Para OCW indicar los temas, que incluirán teoría, problemas, ' 'autoevaluación, etc.'), ) + afectadas = models.TextField(_('Asignatura/s y Titulación/es afectadas'), blank=True, null=True) + formatos = models.TextField(_('Formatos de los materiales incluidos.'), blank=True, null=True) enlace = models.TextField( - _("Enlace"), + _('Enlace'), blank=True, null=True, help_text=_( - "Incluir el enlace (o enlaces) a la página de los estudios en la que se " - "encuentra el plan de mejora y una mención expresa a qué aspecto del mismo " - "se refiere el proyecto." + 'Incluir el enlace (o enlaces) a la página de los estudios en la que se ' + 'encuentra el plan de mejora y una mención expresa a qué aspecto del mismo ' + 'se refiere el proyecto.' ), ) contenido_modulos = models.TextField( - _("Breve descripción de los contenidos de cada capítulo/módulo"), + _('Breve descripción de los contenidos de cada capítulo/módulo'), blank=True, null=True, help_text=_( - "Los cursos 0 deberán incluir un capítulo 0 con las competencias " - "demandadas al alumnado que va a comenzar el estudio o estudios " - "objeto del curso." + 'Los cursos 0 deberán incluir un capítulo 0 con las competencias ' + 'demandadas al alumnado que va a comenzar el estudio o estudios ' + 'objeto del curso.' ), ) - material_previo = models.TextField( - _("Indicar si se cuenta con algún material previo"), blank=True, null=True - ) + material_previo = models.TextField(_('Indicar si se cuenta con algún material previo'), blank=True, null=True) duracion = models.TextField( - _("Duración del curso"), + _('Duración del curso'), blank=True, null=True, help_text=_( - "Número de semanas y número de horas de estudio y trabajo autónomo " - "del participante en todo el curso." + 'Número de semanas y número de horas de estudio y trabajo autónomo ' 'del participante en todo el curso.' ), ) multimedia = models.TextField( - _("Elementos multimedia e innovadores"), - blank=True, - null=True, - help_text=_( - "Elementos multimedia e innovadores que va a utilizar " - "en la elaboración del curso." - ), - ) - indicadores = models.TextField( - _("Indicadores para el seguimiento y evaluación del curso"), + _('Elementos multimedia e innovadores'), blank=True, null=True, + help_text=_('Elementos multimedia e innovadores que va a utilizar ' 'en la elaboración del curso.'), ) + indicadores = models.TextField(_('Indicadores para el seguimiento y evaluación del curso'), blank=True, null=True) actividades = models.TextField( - _("Actividades de dinamización previstas"), - blank=True, - null=True, - help_text=_("Sólo obligatorias para MOOCs."), + _('Actividades de dinamización previstas'), blank=True, null=True, help_text=_('Sólo obligatorias para MOOCs.') ) financiacion = models.TextField( - _("Financiación"), + _('Financiación'), blank=True, null=True, help_text=_( - "Justificar la necesidad de lo solicitado. " - "Añadir información sobre otras fuentes de financiación." + 'Justificar la necesidad de lo solicitado. ' 'Añadir información sobre otras fuentes de financiación.' ), ) ayuda = models.PositiveIntegerField( - _("Ayuda económica solicitada"), + _('Ayuda económica solicitada'), blank=True, null=True, help_text=_( - "Las normas de la convocatoria establecen el importe máximo " - "que se puede solicitar según el programa." + 'Las normas de la convocatoria establecen el importe máximo ' 'que se puede solicitar según el programa.' ), default=0, ) centro = models.ForeignKey( - "Centro", + 'Centro', on_delete=models.PROTECT, blank=True, null=True, - help_text=_("Sólo obligatorio para PIEC, PRACUZ, PIPOUZ."), - related_name="proyectos", - ) - convocatoria = models.ForeignKey("Convocatoria", on_delete=models.PROTECT) - departamento = models.ForeignKey( - "Departamento", on_delete=models.PROTECT, null=True + help_text=_('Sólo obligatorio para PIEC, PRACUZ, PIPOUZ.'), + related_name='proyectos', ) + convocatoria = models.ForeignKey('Convocatoria', on_delete=models.PROTECT) + departamento = models.ForeignKey('Departamento', on_delete=models.PROTECT, null=True) estudio = models.ForeignKey( - "Estudio", + 'Estudio', on_delete=models.PROTECT, blank=True, null=True, - limit_choices_to={"esta_activo": True}, - help_text=_("Sólo obligatorio para PIET."), + limit_choices_to={'esta_activo': True}, + help_text=_('Sólo obligatorio para PIET.'), ) - licencia = models.ForeignKey("Licencia", on_delete=models.PROTECT, null=True) + licencia = models.ForeignKey('Licencia', on_delete=models.PROTECT, null=True) linea = models.ForeignKey( - "Linea", - on_delete=models.PROTECT, - blank=True, - null=True, - verbose_name=_("Línea"), - help_text=_("En su caso."), - ) - programa = models.ForeignKey("Programa", on_delete=models.PROTECT) - visto_bueno_centro = models.BooleanField(_("Visto bueno del centro"), null=True) - visto_bueno_estudio = models.BooleanField( - _("Visto bueno del plan de estudios"), null=True + 'Linea', on_delete=models.PROTECT, blank=True, null=True, verbose_name=_('Línea'), help_text=_('En su caso.') ) + programa = models.ForeignKey('Programa', on_delete=models.PROTECT) + visto_bueno_centro = models.BooleanField(_('Visto bueno del centro'), null=True) + visto_bueno_estudio = models.BooleanField(_('Visto bueno del plan de estudios'), null=True) class Meta: permissions = [ - ("listar_proyectos", _("Puede ver el listado de todos los proyectos.")), - ("ver_proyecto", _("Puede ver cualquier proyecto.")), - ( - "editar_proyecto", - _("Puede editar cualquier proyecto en cualquier momento."), - ), + ('listar_proyectos', _('Puede ver el listado de todos los proyectos.')), + ('ver_proyecto', _('Puede ver cualquier proyecto.')), + ('editar_proyecto', _('Puede editar cualquier proyecto en cualquier momento.')), ] def __str__(self): return self.codigo def get_absolute_url(self): - return reverse("proyecto_detail", args=[str(self.id)]) + return reverse('proyecto_detail', args=[str(self.id)]) def en_borrador(self): - return self.estado == "BORRADOR" + return self.estado == 'BORRADOR' def get_participante_or_none(self, tipo): try: - return ParticipanteProyecto.objects.get( - proyecto_id=self.id, tipo_participacion_id=tipo - ) + return ParticipanteProyecto.objects.get(proyecto_id=self.id, tipo_participacion_id=tipo) except ParticipanteProyecto.DoesNotExist: return None def get_coordinador(self): - """Devuelve el usuario coordinador del proyecto""" - coordinador = self.get_participante_or_none("coordinador") + '''Devuelve el usuario coordinador del proyecto''' + coordinador = self.get_participante_or_none('coordinador') return coordinador.usuario if coordinador else None def get_coordinador_2(self): - """Devuelve el segundo coordinador del proyecto (los PIET pueden tener 2).""" - coordinador_2 = self.get_participante_or_none("coordinador_2") + '''Devuelve el segundo coordinador del proyecto (los PIET pueden tener 2).''' + coordinador_2 = self.get_participante_or_none('coordinador_2') return coordinador_2.usuario if coordinador_2 else None def get_coordinadores(self): - """Devuelve los usuarios coordinadores del proyecto.""" + '''Devuelve los usuarios coordinadores del proyecto.''' coordinadores = [self.get_coordinador(), self.get_coordinador_2()] return list(filter(None, coordinadores)) def get_usuarios_vinculados(self): - """ + ''' Devuelve todos los usuarios vinculados al proyecto (invitados, participantes, etc). - """ + ''' return list(map(lambda p: p.usuario, self.participantes.all())) def tiene_invitados(self): - """Devuelve si el proyecto tiene al menos un invitado.""" - num_invitados = self.participantes.filter(tipo_participacion="invitado").count() + '''Devuelve si el proyecto tiene al menos un invitado.''' + num_invitados = self.participantes.filter(tipo_participacion='invitado').count() return num_invitados >= 1 class Registro(models.Model): fecha = models.DateTimeField(auto_now_add=True) descripcion = models.CharField(max_length=255) - evento = models.ForeignKey("Evento", on_delete=models.PROTECT) - proyecto = models.ForeignKey("Proyecto", on_delete=models.PROTECT) + evento = models.ForeignKey('Evento', on_delete=models.PROTECT) + proyecto = models.ForeignKey('Proyecto', on_delete=models.PROTECT) class TipoEstudio(models.Model): - id = models.PositiveSmallIntegerField(_("Cód. tipo estudio"), primary_key=True) + id = models.PositiveSmallIntegerField(_('Cód. tipo estudio'), primary_key=True) nombre = models.CharField(max_length=63) diff --git a/indo/tables.py b/indo/tables.py index f6f4e28..0f65ce4 100644 --- a/indo/tables.py +++ b/indo/tables.py @@ -8,26 +8,20 @@ from .models import Proyecto class ProyectosTable(tables.Table): def render_titulo(self, record): - enlace = reverse("proyecto_detail", args=[record.id]) + enlace = reverse('proyecto_detail', args=[record.id]) return mark_safe(f"{record.titulo}") - coordinadores = tables.Column( - empty_values=(), orderable=False, verbose_name=_("Coordinador(es)") - ) + coordinadores = tables.Column(empty_values=(), orderable=False, verbose_name=_('Coordinador(es)')) def render_coordinadores(self, record): coordinadores = record.get_coordinadores() - enlaces = [ - f"{c.get_full_name()}" for c in coordinadores - ] - return mark_safe(", ".join(enlaces)) + enlaces = [f"{c.get_full_name()}" for c in coordinadores] + return mark_safe(', '.join(enlaces)) class Meta: - attrs = {"class": "table table-striped table-hover cabecera-azul"} + 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" + 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 diff --git a/indo/templatetags/custom_tags.py b/indo/templatetags/custom_tags.py index 0ea616f..da3ecb6 100644 --- a/indo/templatetags/custom_tags.py +++ b/indo/templatetags/custom_tags.py @@ -13,19 +13,19 @@ register = template.Library() # DEFAULT_TAGS defined in django.contrib.messages.constants MESSAGE_ICONS = { - "debug": '', - "info": '', - "success": '', - "warning": '', - "error": '', + 'debug': '', + 'info': '', + 'success': '', + 'warning': '', + 'error': '', } MESSAGE_STYLES = { - "debug": "alert-info", - "info": "alert-info", - "success": "alert-success", - "warning": "alert-warning", - "error": "alert-danger", + 'debug': 'alert-info', + 'info': 'alert-info', + 'success': 'alert-success', + 'warning': 'alert-warning', + 'error': 'alert-danger', } @@ -41,37 +41,37 @@ def alert_style(tag): @register.simple_tag def lord_url(): - """Devuelve la URL del Single Sign On.""" - return "{base}?{params}".format( - base=reverse("social:begin", kwargs={"backend": "saml"}), - params=urllib.parse.urlencode({"next": "/", "idp": "lord"}), + '''Devuelve la URL del Single Sign On.''' + return '{base}?{params}'.format( + base=reverse('social:begin', kwargs={'backend': 'saml'}), + params=urllib.parse.urlencode({'next': '/', 'idp': 'lord'}), ) @register.filter def get_obj_attr(obj, attr): - """Devuelve el valor del atributo `attr` del objeto `obj`.""" + '''Devuelve el valor del atributo `attr` del objeto `obj`.''' return getattr(obj, attr) @register.filter def get_attr_verbose_name(obj, attr): - """Devuelve el nombre prolijo del atributo indicado.""" + '''Devuelve el nombre prolijo del atributo indicado.''' return obj._meta.get_field(attr).verbose_name -@register.filter(name="has_group") +@register.filter(name='has_group') def has_group(user, group_name): - """Comprueba si el usuario pertenece al grupo indicado.""" + '''Comprueba si el usuario pertenece al grupo indicado.''' return user.groups.filter(name=group_name).exists() # See cleaner = bleach.Cleaner( - tags=(bleach.sanitizer.ALLOWED_TAGS + get_config("ADDITIONAL_ALLOWED_TAGS")), - attributes=get_config("ALLOWED_ATTRIBUTES"), - styles=get_config("ALLOWED_STYLES"), - protocols=get_config("ALLOWED_PROTOCOLS"), + tags=(bleach.sanitizer.ALLOWED_TAGS + get_config('ADDITIONAL_ALLOWED_TAGS')), + attributes=get_config('ALLOWED_ATTRIBUTES'), + styles=get_config('ALLOWED_STYLES'), + protocols=get_config('ALLOWED_PROTOCOLS'), strip=True, strip_comments=True, ) diff --git a/indo/tests.py b/indo/tests.py index b1016a2..0e7efd2 100644 --- a/indo/tests.py +++ b/indo/tests.py @@ -6,39 +6,39 @@ from .views import HomePageView class HomeTests(SimpleTestCase): def test_root_url_resolves_to_homepage_view(self): - found = resolve("/") + found = resolve('/') self.assertEqual(found.func.__name__, HomePageView.as_view().__name__) - self.assertEqual(found.view_name, "home") + self.assertEqual(found.view_name, 'home') def test_home_page_status_code(self): - response = self.client.get("/") + response = self.client.get('/') self.assertEqual(response.status_code, 200) def test_view_url_by_name(self): - resp = self.client.get(reverse("home")) + resp = self.client.get(reverse('home')) self.assertEqual(resp.status_code, 200) def test_view_uses_correct_template(self): - resp = self.client.get(reverse("home")) - self.assertTemplateUsed(resp, "home.html") + resp = self.client.get(reverse('home')) + self.assertTemplateUsed(resp, 'home.html') def test_home_page_returns_correct_html(self): - response = self.client.get("/") - html = response.content.decode("utf-8") - self.assertTrue(html.startswith("")) - self.assertIn("Proyectos de Innovación Docente", html) - self.assertTrue(html.endswith("\n")) + response = self.client.get('/') + html = response.content.decode('utf-8') + self.assertTrue(html.startswith('')) + self.assertIn('Proyectos de Innovación Docente', html) + self.assertTrue(html.endswith('\n')) class AyudaTest(SimpleTestCase): def test_view_url_exists_at_proper_location(self): - response = self.client.get("/ayuda/") + response = self.client.get('/ayuda/') self.assertEqual(response.status_code, 200) def test_view_url_by_name(self): - resp = self.client.get(reverse("ayuda")) + resp = self.client.get(reverse('ayuda')) self.assertEqual(resp.status_code, 200) def test_view_uses_correct_template(self): - resp = self.client.get(reverse("ayuda")) - self.assertTemplateUsed(resp, "ayuda.html") + resp = self.client.get(reverse('ayuda')) + self.assertTemplateUsed(resp, 'ayuda.html') diff --git a/indo/urls.py b/indo/urls.py index 603bd1c..dd655a0 100644 --- a/indo/urls.py +++ b/indo/urls.py @@ -21,61 +21,27 @@ from .views import ( urlpatterns = [ - path("", HomePageView.as_view(), name="home"), - path("summernote/", include("django_summernote.urls")), - path("ayuda/", AyudaView.as_view(), name="ayuda"), + path('', HomePageView.as_view(), name='home'), + path('summernote/', include('django_summernote.urls')), + path('ayuda/', AyudaView.as_view(), name='ayuda'), + path('gestion/proyecto/list/', ProyectoListView.as_view(), name='proyecto_list'), path( - "gestion/proyecto/list/", - ProyectoListView.as_view(), - name="proyecto_list", - ), - path( - "participante-proyecto/aceptar_invitacion/", + 'participante-proyecto/aceptar_invitacion/', ParticipanteAceptarView.as_view(), - name="participante_aceptar", - ), - path( - "participante-proyecto/declinar_invitacion", - ParticipanteDeclinarView.as_view(), - name="participante_declinar", - ), - path( - "participante-proyecto/invitar/", - InvitacionView.as_view(), - name="participante_invitar", - ), - path( - "participante-proyecto/renunciar", - ParticipanteRenunciarView.as_view(), - name="participante_renunciar", - ), - path( - "participante-proyecto//delete/", - ParticipanteDeleteView.as_view(), - name="participante_delete", - ), - path("proyecto/new/", ProyectoCreateView.as_view(), name="proyecto_new"), - path("proyecto//", ProyectoDetailView.as_view(), name="proyecto_detail"), - path( - "proyecto//edit/", - ProyectoUpdateFieldView.as_view(), - name="proyecto_update_field", - ), - path( - "proyecto//anular/", - ProyectoAnularView.as_view(), - name="proyecto_anular", - ), - path( - "proyecto//presentar", - ProyectoPresentarView.as_view(), - name="proyecto_presentar", + name='participante_aceptar', ), path( - "proyecto/mis-proyectos/", - ProyectosUsuarioView.as_view(), - name="mis_proyectos", + 'participante-proyecto/declinar_invitacion', ParticipanteDeclinarView.as_view(), name='participante_declinar' ), + path('participante-proyecto/invitar/', InvitacionView.as_view(), name='participante_invitar'), + path('participante-proyecto/renunciar', ParticipanteRenunciarView.as_view(), name='participante_renunciar'), + path('participante-proyecto//delete/', ParticipanteDeleteView.as_view(), name='participante_delete'), + path('proyecto/new/', ProyectoCreateView.as_view(), name='proyecto_new'), + path('proyecto//', ProyectoDetailView.as_view(), name='proyecto_detail'), + path('proyecto//edit/', ProyectoUpdateFieldView.as_view(), name='proyecto_update_field'), + path('proyecto//anular/', ProyectoAnularView.as_view(), name='proyecto_anular'), + path('proyecto//presentar', ProyectoPresentarView.as_view(), name='proyecto_presentar'), + path('proyecto/mis-proyectos/', ProyectosUsuarioView.as_view(), name='mis_proyectos'), ] if settings.DEBUG: diff --git a/indo/views.py b/indo/views.py index c94b12a..07d30ad 100644 --- a/indo/views.py +++ b/indo/views.py @@ -6,11 +6,7 @@ import pypandoc from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import ( - LoginRequiredMixin, - PermissionRequiredMixin, - 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 @@ -28,184 +24,156 @@ from django_tables2.views import SingleTableView from templated_email import send_templated_mail from .forms import InvitacionForm, ProyectoForm -from .models import ( - Centro, - Convocatoria, - Evento, - ParticipanteProyecto, - Plan, - Proyecto, - Registro, - TipoParticipacion, -) +from .models import Centro, Convocatoria, Evento, ParticipanteProyecto, Plan, Proyecto, Registro, TipoParticipacion from .tables import ProyectosTable class ChecksMixin(UserPassesTestMixin): - """Proporciona comprobaciones para autorizar o no una acción a un usuario.""" + '''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.""" + '''Devuelve si el usuario actual es coordinador del proyecto indicado.''' proyecto = get_object_or_404(Proyecto, pk=proyecto_id) usuario_actual = self.request.user coordinadores_participantes = proyecto.participantes.filter( - tipo_participacion__in=["coordinador", "coordinador_2"] + tipo_participacion__in=['coordinador', 'coordinador_2'] ).all() - usuarios_coordinadores = list( - map(lambda p: p.usuario, coordinadores_participantes) - ) - self.permission_denied_message = _("Usted no es coordinador de este proyecto.") + 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.""" + '''Devuelve si el usuario actual es participante del proyecto indicado.''' proyecto = get_object_or_404(Proyecto, pk=proyecto_id) usuario_actual = self.request.user - pp = proyecto.participantes.filter( - usuario=usuario_actual, tipo_participacion="participante" - ).all() - self.permission_denied_message = _("Usted no es participante de este proyecto.") + pp = proyecto.participantes.filter(usuario=usuario_actual, tipo_participacion='participante').all() + self.permission_denied_message = _('Usted no es participante de este proyecto.') return True if pp else False def es_invitado(self, proyecto_id): - """Devuelve si el usuario actual es invitado del proyecto indicado.""" + '''Devuelve si el usuario actual es invitado del proyecto indicado.''' proyecto = get_object_or_404(Proyecto, pk=proyecto_id) usuario_actual = self.request.user - pp = proyecto.participantes.filter( - usuario=usuario_actual, tipo_participacion="invitado" - ).all() - self.permission_denied_message = _("Usted no está invitado a este proyecto.") + pp = proyecto.participantes.filter(usuario=usuario_actual, tipo_participacion='invitado').all() + self.permission_denied_message = _('Usted no está invitado a este proyecto.') return True if pp else False def esta_vinculado(self, proyecto_id): - """Devuelve si el usuario actual está vinculado al proyecto indicado.""" + '''Devuelve si el usuario actual está vinculado al proyecto indicado.''' proyecto = get_object_or_404(Proyecto, pk=proyecto_id) usuario_actual = self.request.user pp = ( proyecto.participantes.filter(usuario=usuario_actual) - .exclude(tipo_participacion="invitacion_rehusada") + .exclude(tipo_participacion='invitacion_rehusada') .all() ) - self.permission_denied_message = _("Usted no está vinculado a este proyecto.") + self.permission_denied_message = _('Usted no está vinculado a este proyecto.') return True if pp else False def es_pas_o_pdi(self): - """ + ''' Devuelve si el usuario actual es PAS o PDI de la UZ o de sus centros adscritos. - """ + ''' usuario_actual = self.request.user colectivos_del_usuario = json.loads(usuario_actual.colectivos) - self.permission_denied_message = _("Usted no es PAS ni PDI.") + self.permission_denied_message = _('Usted no es PAS ni PDI.') - return any( - col_autorizado in colectivos_del_usuario - for col_autorizado in ["PAS", "ADS", "PDI"] - ) + return any(col_autorizado in colectivos_del_usuario for col_autorizado in ['PAS', 'ADS', 'PDI']) def es_decano_o_director(self, proyecto_id): - """Devuelve si el usuario actual es decano/director del centro del proyecto.""" + '''Devuelve si el usuario actual es decano/director del centro del proyecto.''' usuario_actual = self.request.user proyecto = get_object_or_404(Proyecto, pk=proyecto_id) centro = proyecto.centro if not centro: return False nip_decano = centro.nip_decano - self.permission_denied_message = _( - "Usted no es decano/director del centro del proyecto." - ) + self.permission_denied_message = _('Usted no es decano/director del centro del proyecto.') return usuario_actual.username == str(nip_decano) def esta_vinculado_o_es_decano_o_es_coordinador(self, proyecto_id): - """ + ''' Devuelve si el usuario actual está vinculado al proyecto indicado o es decano o director del centro del proyecto - o es coordinador del plan de estudios del proyecto.""" + o es coordinador del plan de estudios del proyecto.''' usuario_actual = self.request.user esta_autorizado = ( self.esta_vinculado(proyecto_id) or self.es_decano_o_director(proyecto_id) or self.es_coordinador_estudio(proyecto_id) - or usuario_actual.has_perm("indo.ver_proyecto") # Gestores y evaluadores + or usuario_actual.has_perm('indo.ver_proyecto') # Gestores y evaluadores ) self.permission_denied_message = _( - "Usted no está vinculado a este proyecto, " - "ni es decano/director del centro del proyecto, " - "ni es coordinador del plan de estudios del proyecto." + 'Usted no está vinculado a este proyecto, ' + 'ni es decano/director del centro del proyecto, ' + 'ni es coordinador del plan de estudios del proyecto.' ) return esta_autorizado def es_coordinador_estudio(self, proyecto_id): - """Devuelve si el usuario actual es coordinador del estudio del proyecto.""" + '''Devuelve si el usuario actual es coordinador del estudio del proyecto.''' usuario_actual = self.request.user proyecto = get_object_or_404(Proyecto, pk=proyecto_id) estudio = proyecto.estudio if not estudio: return False - nip_coordinadores = [ - f"{p.nip_coordinador}" for p in estudio.planes.all() if p.nip_coordinador - ] + nip_coordinadores = [f'{p.nip_coordinador}' for p in estudio.planes.all() if p.nip_coordinador] - self.permission_denied_message = _( - "Usted no es coordinador del plan de estudios del proyecto." - ) + self.permission_denied_message = _('Usted no es coordinador del plan de estudios del proyecto.') return usuario_actual.username in nip_coordinadores class AyudaView(TemplateView): - template_name = "ayuda.html" + template_name = 'ayuda.html' class HomePageView(TemplateView): - template_name = "home.html" + template_name = 'home.html' class InvitacionView(LoginRequiredMixin, ChecksMixin, CreateView): - """Muestra un formulario para invitar a una persona a un proyecto determinado.""" + '''Muestra un formulario para invitar a una persona a un proyecto determinado.''' form_class = InvitacionForm model = ParticipanteProyecto - template_name = "participante-proyecto/invitar.html" + 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) + 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 kwargs.update(self.kwargs) # self.kwargs contains all URL conf params - kwargs["request"] = self.request + kwargs['request'] = self.request return kwargs def get_success_url(self, **kwargs): - return reverse_lazy( - "proyecto_detail", kwargs={"pk": self.kwargs["proyecto_id"]} - ) + return reverse_lazy('proyecto_detail', kwargs={'pk': self.kwargs['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") + return self.es_coordinador(self.kwargs['proyecto_id']) or self.request.user.has_perm('indo.editar_proyecto') class ParticipanteAceptarView(LoginRequiredMixin, RedirectView): - """Aceptar la invitación a participar en un proyecto.""" + '''Aceptar la invitación a participar en un proyecto.''' def get_redirect_url(self, *args, **kwargs): - return reverse_lazy("mis_proyectos", kwargs={"anyo": date.today().year}) + return reverse_lazy('mis_proyectos', kwargs={'anyo': date.today().year}) def post(self, request, *args, **kwargs): usuario_actual = self.request.user - proyecto_id = kwargs.get("proyecto_id") + proyecto_id = kwargs.get('proyecto_id') proyecto = get_object_or_404(Proyecto, pk=proyecto_id) num_equipos = usuario_actual.get_num_equipos(proyecto.convocatoria_id) @@ -214,102 +182,82 @@ class ParticipanteAceptarView(LoginRequiredMixin, RedirectView): messages.error( request, _( - f"""No puede aceptar esta invitación porque ya forma parte del número + f'''No puede aceptar esta invitación porque ya forma parte del número máximo de equipos de trabajo permitido ({num_max_equipos}). Para poder aceptar esta invitación, antes debería renunciar a participar - en algún otro proyecto.""" + en algún otro proyecto.''' ), ) return super().post(request, *args, **kwargs) pp = get_object_or_404( - ParticipanteProyecto, - proyecto_id=proyecto_id, - usuario=usuario_actual, - tipo_participacion="invitado", + ParticipanteProyecto, proyecto_id=proyecto_id, usuario=usuario_actual, tipo_participacion='invitado' ) - pp.tipo_participacion_id = "participante" + pp.tipo_participacion_id = 'participante' pp.save() - messages.success( - request, - _(f"Ha pasado a ser participante del proyecto «{proyecto.titulo}»."), - ) + 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.""" + '''Declinar la invitación a participar en un proyecto.''' def get_redirect_url(self, *args, **kwargs): - return reverse_lazy("mis_proyectos", kwargs={"anyo": date.today().year}) + return reverse_lazy('mis_proyectos', kwargs={'anyo': date.today().year}) def post(self, request, *args, **kwargs): - proyecto_id = request.POST.get("proyecto_id") + 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", + ParticipanteProyecto, proyecto_id=proyecto_id, usuario=usuario_actual, tipo_participacion='invitado' ) - pp.tipo_participacion_id = "invitacion_rehusada" + pp.tipo_participacion_id = 'invitacion_rehusada' pp.save() - messages.success( - request, - _(f"Ha rehusado ser participante del proyecto «{proyecto.titulo}»."), - ) + messages.success(request, _(f'Ha rehusado ser participante del proyecto «{proyecto.titulo}».')) return super().post(request, *args, **kwargs) class ParticipanteRenunciarView(LoginRequiredMixin, RedirectView): - """Renunciar a participar en un proyecto.""" + '''Renunciar a participar en un proyecto.''' def get_redirect_url(self, *args, **kwargs): - return reverse_lazy("mis_proyectos", kwargs={"anyo": date.today().year}) + return reverse_lazy('mis_proyectos', kwargs={'anyo': date.today().year}) def post(self, request, *args, **kwargs): - proyecto_id = request.POST.get("proyecto_id") + 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="participante", + ParticipanteProyecto, proyecto_id=proyecto_id, usuario=usuario_actual, tipo_participacion='participante' ) - pp.tipo_participacion_id = "invitacion_rehusada" + pp.tipo_participacion_id = 'invitacion_rehusada' pp.save() - messages.success( - request, - _(f"Ha renunciado a participar en el proyecto «{proyecto.titulo}»."), - ) + messages.success(request, _(f'Ha renunciado a participar en el proyecto «{proyecto.titulo}».')) return super().post(request, *args, **kwargs) class ParticipanteDeleteView(LoginRequiredMixin, ChecksMixin, DeleteView): - """Borra un registro de ParticipanteProyecto""" + '''Borra un registro de ParticipanteProyecto''' model = ParticipanteProyecto - template_name = "participante-proyecto/confirm_delete.html" + template_name = 'participante-proyecto/confirm_delete.html' def get_success_url(self): - return reverse_lazy("proyecto_detail", args=[self.object.proyecto.id]) + return reverse_lazy('proyecto_detail', args=[self.object.proyecto.id]) def test_func(self): - return self.es_coordinador( - self.get_object().proyecto.id - ) or self.request.user.has_perm("indo.editar_proyecto") + return self.es_coordinador(self.get_object().proyecto.id) or self.request.user.has_perm('indo.editar_proyecto') class ProyectoCreateView(LoginRequiredMixin, ChecksMixin, CreateView): - """Crea una nueva solicitud de proyecto""" + '''Crea una nueva solicitud de proyecto''' model = Proyecto - template_name = "proyecto/new.html" + template_name = 'proyecto/new.html' form_class = ProyectoForm def form_valid(self, form): @@ -318,13 +266,13 @@ class ProyectoCreateView(LoginRequiredMixin, ChecksMixin, CreateView): proyecto = form.save() self._guardar_coordinador(proyecto) self._registrar_creacion(proyecto) - return redirect("proyecto_detail", proyecto.id) + return redirect('proyecto_detail', proyecto.id) def get_form(self, form_class=None): - """ + ''' Devuelve el formulario añadiendo automáticamente el campo Convocatoria, que es requerido, y el usuario, para comprobar si tiene los permisos necesarios. - """ + ''' form = super(ProyectoCreateView, self).get_form(form_class) form.instance.user = self.request.user form.instance.convocatoria = Convocatoria(date.today().year) @@ -332,19 +280,13 @@ class ProyectoCreateView(LoginRequiredMixin, ChecksMixin, CreateView): def _guardar_coordinador(self, proyecto): pp = ParticipanteProyecto( - proyecto=proyecto, - tipo_participacion=TipoParticipacion(nombre="coordinador"), - usuario=self.request.user, + proyecto=proyecto, tipo_participacion=TipoParticipacion(nombre='coordinador'), usuario=self.request.user ) pp.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, - ) + evento = Evento.objects.get(nombre='creacion_solicitud') + registro = Registro(descripcion='Creación inicial de la solicitud', evento=evento, proyecto=proyecto) registro.save() def test_func(self): @@ -354,106 +296,104 @@ class ProyectoCreateView(LoginRequiredMixin, ChecksMixin, CreateView): class ProyectoAnularView(LoginRequiredMixin, ChecksMixin, RedirectView): - """ + ''' Cambia el estado de una solicitud de proyecto a Anulada. - """ + ''' model = Proyecto def get_redirect_url(self, *args, **kwargs): - return reverse_lazy("mis_proyectos", kwargs={"anyo": date.today().year}) + return reverse_lazy('mis_proyectos', kwargs={'anyo': date.today().year}) def post(self, request, *args, **kwargs): - proyecto = Proyecto.objects.get(pk=kwargs.get("pk")) - proyecto.estado = "ANULADO" + proyecto = Proyecto.objects.get(pk=kwargs.get('pk')) + proyecto.estado = 'ANULADO' proyecto.save() - messages.success(request, _("Su solicitud de proyecto ha sido anulada.")) + messages.success(request, _('Su solicitud de proyecto ha sido anulada.')) return super().post(request, *args, **kwargs) def test_func(self): - return self.es_coordinador(self.kwargs["pk"]) + return self.es_coordinador(self.kwargs['pk']) class ProyectoDetailView(LoginRequiredMixin, ChecksMixin, DetailView): - """Muestra una solicitud de proyecto.""" + '''Muestra una solicitud de proyecto.''' model = Proyecto - template_name = "proyecto/detail.html" + template_name = 'proyecto/detail.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - pp_coordinador = self.object.get_participante_or_none("coordinador") - context["pp_coordinador"] = pp_coordinador + pp_coordinador = self.object.get_participante_or_none('coordinador') + context['pp_coordinador'] = pp_coordinador - pp_coordinador_2 = self.object.get_participante_or_none("coordinador_2") - context["pp_coordinador_2"] = pp_coordinador_2 + pp_coordinador_2 = self.object.get_participante_or_none('coordinador_2') + context['pp_coordinador_2'] = pp_coordinador_2 participantes = ( - self.object.participantes.filter(tipo_participacion="participante") - .order_by("usuario__first_name", "usuario__last_name") + self.object.participantes.filter(tipo_participacion='participante') + .order_by('usuario__first_name', 'usuario__last_name') .all() ) - context["participantes"] = participantes + context['participantes'] = participantes invitados = ( - self.object.participantes.filter( - tipo_participacion__in=["invitado", "invitacion_rehusada"] - ) - .order_by("tipo_participacion", "usuario__first_name", "usuario__last_name") + self.object.participantes.filter(tipo_participacion__in=['invitado', 'invitacion_rehusada']) + .order_by('tipo_participacion', 'usuario__first_name', 'usuario__last_name') .all() ) - context["invitados"] = invitados + context['invitados'] = invitados - context["campos"] = json.loads(self.object.programa.campos) + context['campos'] = json.loads(self.object.programa.campos) - context["permitir_edicion"] = ( + context['permitir_edicion'] = ( self.es_coordinador(self.object.id) and self.object.en_borrador() - ) or self.request.user.has_perm("indo.editar_proyecto") + ) or self.request.user.has_perm('indo.editar_proyecto') - context["es_editor"] = self.request.user.has_perm("indo.editar_proyecto") + context['es_editor'] = self.request.user.has_perm('indo.editar_proyecto') return context def test_func(self): - proyecto_id = self.kwargs["pk"] + proyecto_id = self.kwargs['pk'] return self.esta_vinculado_o_es_decano_o_es_coordinador(proyecto_id) class ProyectoListView(LoginRequiredMixin, PermissionRequiredMixin, SingleTableView): - """Muestra una tabla de todos los proyectos presentados en una convocatoria.""" + '''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.") + 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" + template_name = 'gestion/proyecto/tabla.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["anyo"] = self.kwargs["anyo"] + 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") + 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. + '''Presenta una solicitud de proyecto. El proyecto pasa de estado «Borrador» a estado «Solicitado». Se envían correos a los agentes involucrados. - """ + ''' def get_redirect_url(self, *args, **kwargs): - return reverse_lazy("proyecto_detail", args=[kwargs.get("pk")]) + return reverse_lazy('proyecto_detail', args=[kwargs.get('pk')]) def post(self, request, *args, **kwargs): - proyecto_id = kwargs.get("pk") + proyecto_id = kwargs.get('pk') proyecto = Proyecto.objects.get(pk=proyecto_id) # TODO ¿Chequear el estado actual del proyecto? @@ -464,21 +404,18 @@ class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView): messages.error( request, _( - f"""No puede presentar esta solicitud porque ya forma parte + f'''No puede presentar esta solicitud porque ya forma parte del número máximo de equipos de trabajo permitido ({num_max_equipos}). Para poder presentar esta solicitud de proyecto, antes debería renunciar - a participar en algún otro proyecto.""" + a participar en algún otro proyecto.''' ), ) return super().post(request, *args, **kwargs) - if request.user.get_colectivo_principal() == "ADS" and proyecto.ayuda != 0: + if request.user.get_colectivo_principal() == 'ADS' and proyecto.ayuda != 0: messages.error( request, - _( - "Los profesores de los centros adscritos no pueden coordinar " - "proyectos con financiación." - ), + _('Los profesores de los centros adscritos no pueden coordinar ' 'proyectos con financiación.'), ) return super().post(request, *args, **kwargs) @@ -486,17 +423,14 @@ class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView): messages.error( request, _( - f"La ayuda solicitada ({proyecto.ayuda} €) excede el máximo " - f"permitido para este programa ({proyecto.programa.max_ayuda} €)." + f'La ayuda solicitada ({proyecto.ayuda} €) excede el máximo ' + f'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."), - ) + messages.error(request, _('La solicitud debe incluir al menos un invitado a participar.')) return super().post(request, *args, **kwargs) self._enviar_invitaciones(request, proyecto) @@ -509,69 +443,63 @@ class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView): # TODO Enviar "resguardo" al solicitante. PDF? - proyecto.estado = "SOLICITADO" + 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.")) + messages.success(request, _('Su solicitud de proyecto ha sido presentada.')) return super().post(request, *args, **kwargs) def _enviar_invitaciones(self, request, proyecto): - """Envia un mensaje a cada uno de los invitados al proyecto.""" - for invitado in proyecto.participantes.filter(tipo_participacion="invitado"): + '''Envia un mensaje a cada uno de los invitados al proyecto.''' + for invitado in proyecto.participantes.filter(tipo_participacion='invitado'): send_templated_mail( - template_name="invitacion", + 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, - "programa_proyecto": f"{proyecto.programa.nombre_corto} " - + f"({proyecto.programa.nombre_largo})", - "descripcion_proyecto": pypandoc.convert_text( - proyecto.descripcion, "md", format="html" - ).replace("\\\n", " \n"), - "site_url": settings.SITE_URL, + 'nombre_coordinador': request.user.get_full_name(), + 'nombre_invitado': invitado.usuario.get_full_name(), + 'sexo_invitado': invitado.usuario.sexo, + 'titulo_proyecto': proyecto.titulo, + 'programa_proyecto': f'{proyecto.programa.nombre_corto} ' + f'({proyecto.programa.nombre_largo})', + 'descripcion_proyecto': pypandoc.convert_text(proyecto.descripcion, 'md', format='html').replace( + '\\\n', ' \n' + ), + 'site_url': settings.SITE_URL, }, ) def _enviar_solicitudes_visto_bueno_centro(self, request, proyecto): - """Envia un mensaje al responsable del centro solicitando su visto bueno.""" + '''Envia un mensaje al responsable del centro solicitando su visto bueno.''' try: validate_email(proyecto.centro.email_decano) except ValidationError: messages.warning( - request, - _( - "La dirección de correo electrónico del director o decano " - "del centro no es válida." - ), + request, _('La dirección de correo electrónico del director o decano ' 'del centro no es válida.') ) return send_templated_mail( - template_name="solicitud_visto_bueno_centro", + template_name='solicitud_visto_bueno_centro', 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, - "programa_proyecto": f"{proyecto.programa.nombre_corto} " - f"({proyecto.programa.nombre_largo})", - "descripcion_proyecto": pypandoc.convert_text( - proyecto.descripcion, "md", format="html" - ).replace("\\\n", "\n"), - "site_url": settings.SITE_URL, + 'nombre_coordinador': request.user.get_full_name(), + 'nombre_decano': proyecto.centro.nombre_decano, + 'tratamiento_decano': proyecto.centro.tratamiento_decano, + 'titulo_proyecto': proyecto.titulo, + 'programa_proyecto': f'{proyecto.programa.nombre_corto} ' f'({proyecto.programa.nombre_largo})', + 'descripcion_proyecto': pypandoc.convert_text(proyecto.descripcion, 'md', format='html').replace( + '\\\n', '\n' + ), + 'site_url': settings.SITE_URL, }, ) def _is_email_valid(self, email): - """Validate email address""" + '''Validate email address''' try: validate_email(email) @@ -580,80 +508,62 @@ class ProyectoPresentarView(LoginRequiredMixin, ChecksMixin, RedirectView): return True def _enviar_solicitudes_visto_bueno_estudio(self, request, proyecto): - """Envia mensaje a los coordinadores del plan solicitando su visto bueno.""" + '''Envia mensaje a los coordinadores del plan solicitando su visto bueno.''' email_coordinadores_estudio = [ - f"{p.email_coordinador}" + f'{p.email_coordinador}' for p in proyecto.estudio.planes.all() if self._is_email_valid(p.email_coordinador) ] send_templated_mail( - template_name="solicitud_visto_bueno_estudio", + template_name='solicitud_visto_bueno_estudio', from_email=None, # settings.DEFAULT_FROM_EMAIL recipient_list=email_coordinadores_estudio, context={ - "nombre_coordinador": request.user.get_full_name(), - "titulo_proyecto": proyecto.titulo, - "programa_proyecto": f"{proyecto.programa.nombre_corto} " - f"({proyecto.programa.nombre_largo})", - "descripcion_proyecto": pypandoc.convert_text( - proyecto.descripcion, "md", format="html" - ).replace("\\\n", "\n"), - "site_url": settings.SITE_URL, + 'nombre_coordinador': request.user.get_full_name(), + 'titulo_proyecto': proyecto.titulo, + 'programa_proyecto': f'{proyecto.programa.nombre_corto} ' f'({proyecto.programa.nombre_largo})', + 'descripcion_proyecto': pypandoc.convert_text(proyecto.descripcion, 'md', format='html').replace( + '\\\n', '\n' + ), + 'site_url': settings.SITE_URL, }, ) def test_func(self): # TODO: Comprobar fecha - return self.es_coordinador(self.kwargs["pk"]) + return self.es_coordinador(self.kwargs['pk']) class ProyectoUpdateFieldView(LoginRequiredMixin, ChecksMixin, UpdateView): - """Actualiza un campo de una solicitud de proyecto.""" + '''Actualiza un campo de una solicitud de proyecto.''' # TODO: Comprobar estado/fecha model = Proyecto - template_name = "proyecto/update.html" + template_name = 'proyecto/update.html' def get_form_class(self, **kwargs): - campo = self.kwargs["campo"] - if campo in ( - "centro", - "codigo", - "convocatoria", - "estado", - "estudio", - "linea", - "programa", - ): - raise Http404(_("No puede editar ese campo.")) - - if campo not in ( - "titulo", - "departamento", - "licencia", - "ayuda", - "visto_bueno_centro", - "visto_bueno_estudio", - ): - formulario = modelform_factory( - Proyecto, fields=(campo,), widgets={campo: SummernoteWidget()} - ) + campo = self.kwargs['campo'] + if campo in ('centro', 'codigo', 'convocatoria', 'estado', 'estudio', 'linea', 'programa'): + raise Http404(_('No puede editar ese campo.')) + + if campo not in ('titulo', 'departamento', 'licencia', 'ayuda', 'visto_bueno_centro', 'visto_bueno_estudio'): + formulario = modelform_factory(Proyecto, fields=(campo,), widgets={campo: SummernoteWidget()}) def as_p(self): - """ + ''' Return this form rendered as HTML

s, with the helptext over the textarea. - """ + ''' return self._html_output( - normal_row=""" + normal_row=''' %(label)s %(help_text)s %(field)s -

""", - error_row="%s", - row_ender="

", +

''', + error_row='%s', + row_ender='

', help_text_html='%s', errors_on_separate_row=True, ) @@ -667,13 +577,10 @@ class ProyectoUpdateFieldView(LoginRequiredMixin, ChecksMixin, UpdateView): cleaned_data[campo] = mark_safe( bleach.clean( texto, - tags=( - bleach.sanitizer.ALLOWED_TAGS - + get_config("ADDITIONAL_ALLOWED_TAGS") - ), - attributes=get_config("ALLOWED_ATTRIBUTES"), - styles=get_config("ALLOWED_STYLES"), - protocols=get_config("ALLOWED_PROTOCOLS"), + tags=(bleach.sanitizer.ALLOWED_TAGS + get_config('ADDITIONAL_ALLOWED_TAGS')), + attributes=get_config('ALLOWED_ATTRIBUTES'), + styles=get_config('ALLOWED_STYLES'), + protocols=get_config('ALLOWED_PROTOCOLS'), strip=True, ) ) @@ -686,60 +593,49 @@ class ProyectoUpdateFieldView(LoginRequiredMixin, ChecksMixin, UpdateView): return super().get_form_class() def test_func(self): - """Devuelve si el usuario está autorizado a modificar este campo.""" + '''Devuelve si el usuario está autorizado a modificar este campo.''' return ( - self.es_coordinador(self.kwargs["pk"]) - or ( - self.kwargs["campo"] == "visto_bueno_centro" - and self.es_decano_o_director(self.kwargs["pk"]) - ) - or ( - self.kwargs["campo"] == "visto_bueno_estudio" - and self.es_coordinador_estudio(self.kwargs["pk"]) - ) - or self.request.user.has_perm("indo.editar_proyecto") + self.es_coordinador(self.kwargs['pk']) + or (self.kwargs['campo'] == 'visto_bueno_centro' and self.es_decano_o_director(self.kwargs['pk'])) + or (self.kwargs['campo'] == 'visto_bueno_estudio' and self.es_coordinador_estudio(self.kwargs['pk'])) + or self.request.user.has_perm('indo.editar_proyecto') ) class ProyectosUsuarioView(LoginRequiredMixin, TemplateView): - """Lista los proyectos a los que está vinculado el usuario actual.""" + '''Lista los proyectos a los que está vinculado el usuario actual.''' - template_name = "proyecto/mis-proyectos.html" + template_name = 'proyecto/mis-proyectos.html' def get_context_data(self, **kwargs): usuario = self.request.user - anyo = self.kwargs["anyo"] + anyo = self.kwargs['anyo'] context = super().get_context_data(**kwargs) - context["proyectos_coordinados"] = ( + context['proyectos_coordinados'] = ( Proyecto.objects.filter( convocatoria__id=anyo, participantes__usuario=usuario, - participantes__tipo_participacion_id__in=[ - "coordinador", - "coordinador_2", - ], + participantes__tipo_participacion_id__in=['coordinador', 'coordinador_2'], ) - .exclude(estado="ANULADO") - .order_by("programa__nombre_corto", "linea__nombre", "titulo") + .exclude(estado='ANULADO') + .order_by('programa__nombre_corto', 'linea__nombre', 'titulo') .all() ) - context["proyectos_participados"] = ( + context['proyectos_participados'] = ( Proyecto.objects.filter( convocatoria__id=anyo, participantes__usuario=usuario, - participantes__tipo_participacion_id="participante", + participantes__tipo_participacion_id='participante', ) - .order_by("programa__nombre_corto", "linea__nombre", "titulo") + .order_by('programa__nombre_corto', 'linea__nombre', 'titulo') .all() ) - context["proyectos_invitado"] = ( + context['proyectos_invitado'] = ( Proyecto.objects.filter( - convocatoria__id=anyo, - participantes__usuario=usuario, - participantes__tipo_participacion_id="invitado", + convocatoria__id=anyo, participantes__usuario=usuario, participantes__tipo_participacion_id='invitado' ) - .order_by("programa__nombre_corto", "linea__nombre", "titulo") + .order_by('programa__nombre_corto', 'linea__nombre', 'titulo') .all() ) @@ -750,16 +646,14 @@ class ProyectosUsuarioView(LoginRequiredMixin, TemplateView): centros_dirigidos = Centro.objects.filter(nip_decano=nip_usuario).all() if centros_dirigidos: - context["proyectos_centros_dirigidos"] = Proyecto.objects.filter( - convocatoria_id=anyo, - programa__requiere_visto_bueno_centro=True, - centro__in=centros_dirigidos, + context['proyectos_centros_dirigidos'] = Proyecto.objects.filter( + convocatoria_id=anyo, programa__requiere_visto_bueno_centro=True, centro__in=centros_dirigidos ).all() planes_coordinados = Plan.objects.filter(nip_coordinador=nip_usuario).all() if planes_coordinados: id_estudios_coordinados = set([p.estudio_id for p in planes_coordinados]) - context["proyectos_estudios_coordinados"] = Proyecto.objects.filter( + context['proyectos_estudios_coordinados'] = Proyecto.objects.filter( convocatoria_id=anyo, programa__requiere_visto_bueno_estudio=True, estudio_id__in=id_estudios_coordinados, diff --git a/manage.py b/manage.py index 9c40287..411c9cf 100755 --- a/manage.py +++ b/manage.py @@ -3,14 +3,14 @@ import os import sys -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manhattan_project.settings") +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'manhattan_project.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' ) from exc execute_from_command_line(sys.argv) diff --git a/manhattan_project/settings-sample.py b/manhattan_project/settings-sample.py index dbd088d..40dbcd3 100644 --- a/manhattan_project/settings-sample.py +++ b/manhattan_project/settings-sample.py @@ -1,4 +1,4 @@ -""" +''' Django settings for manhattan_project project. Generated by 'django-admin startproject' using Django 2.1. @@ -8,7 +8,7 @@ https://docs.djangoproject.com/en/2.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.1/ref/settings/ -""" +''' import os from datetime import date @@ -24,22 +24,20 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get( - "DJANGO_SECRET_KEY", "xk6ujnt_zj7xlnt@c&$jc9f_=u3io5e!87imbqz4)=li*$tu%w" -) +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'xk6ujnt_zj7xlnt@c&$jc9f_=u3io5e!87imbqz4)=li*$tu%w') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.environ.get("DEBUG", False) == "True" +DEBUG = os.environ.get('DEBUG', False) == 'True' ALLOWED_HOSTS = [] # ['*'] -DEFAULT_FROM_EMAIL = "La Maestra " +DEFAULT_FROM_EMAIL = 'La Maestra ' # Production value: 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" -EMAIL_HOST = "smtp.manhattan.local" -EMAIL_HOST_USER = "mls" -EMAIL_HOST_PASSWORD = "plaff" +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_HOST = 'smtp.manhattan.local' +EMAIL_HOST_USER = 'mls' +EMAIL_HOST_PASSWORD = 'plaff' EMAIL_PORT = 587 EMAIL_USE_LOCALTIME = True EMAIL_USE_TLS = True @@ -48,72 +46,70 @@ EMAIL_USE_TLS = True # Application definition INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', # Local - "indo.apps.IndoConfig", - "accounts.apps.AccountsConfig", + 'indo.apps.IndoConfig', + 'accounts.apps.AccountsConfig', # 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 + '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 ] MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = "manhattan_project.urls" +ROOT_URLCONF = 'manhattan_project.urls' -SITE_URL = "http://manhattan.local/" +SITE_URL = 'http://manhattan.local/' TEMPLATES = [ { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "templates")], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "social_django.context_processors.backends", - "social_django.context_processors.login_redirect", + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'social_django.context_processors.backends', + 'social_django.context_processors.login_redirect', ] }, } ] -WSGI_APPLICATION = "manhattan_project.wsgi.application" +WSGI_APPLICATION = 'manhattan_project.wsgi.application' # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { - "default": { - "ENGINE": "django.db.backends.mysql", # Database engine - "NAME": os.environ.get("DB_NAME"), # Database name - "USER": os.environ.get("DB_USER"), # Database user - "PASSWORD": os.environ.get("DB_PASSWORD"), # Database password - "HOST": os.environ.get("DB_HOST"), # Set to empty string for localhost. - "PORT": "", # Set to empty string for default. + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Database engine + 'NAME': os.environ.get('DB_NAME'), # Database name + 'USER': os.environ.get('DB_USER'), # Database user + 'PASSWORD': os.environ.get('DB_PASSWORD'), # Database password + 'HOST': os.environ.get('DB_HOST'), # Set to empty string for localhost. + 'PORT': '', # Set to empty string for default. # Additional database options - "OPTIONS": { - "charset": "utf8mb4" # Requires `innodb_default_row_format = dynamic` - }, + 'OPTIONS': {'charset': 'utf8mb4'}, # Requires `innodb_default_row_format = dynamic` } } @@ -122,21 +118,19 @@ DATABASES = { # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" # noqa: E501 - }, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, - {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, # noqa: E501 + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, ] # Internationalization # https://docs.djangoproject.com/en/2.1/topics/i18n/ -LANGUAGE_CODE = "es-es" +LANGUAGE_CODE = 'es-es' -TIME_ZONE = "Europe/Madrid" +TIME_ZONE = 'Europe/Madrid' USE_I18N = True @@ -148,131 +142,118 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ -STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") -STATIC_URL = "/static/" -STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] -STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') +STATIC_URL = '/static/' +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' -AUTH_USER_MODEL = "accounts.CustomUser" -LOGIN_URL = f"{SITE_URL}login/saml/?idp=lord" -LOGIN_REDIRECT_URL = reverse_lazy("mis_proyectos", args=[date.today().year]) -LOGOUT_REDIRECT_URL = "home" +AUTH_USER_MODEL = 'accounts.CustomUser' +LOGIN_URL = f'{SITE_URL}login/saml/?idp=lord' +LOGIN_REDIRECT_URL = reverse_lazy('mis_proyectos', args=[date.today().year]) +LOGOUT_REDIRECT_URL = 'home' # ## SAML with Python Social Auth ## # # https://python-social-auth.readthedocs.io/en/latest/backends/saml.html -AUTHENTICATION_BACKENDS = ( - "social_core.backends.saml.SAMLAuth", - "django.contrib.auth.backends.ModelBackend", -) +AUTHENTICATION_BACKENDS = ('social_core.backends.saml.SAMLAuth', 'django.contrib.auth.backends.ModelBackend') # When using PostgreSQL, # it’s recommended to use the built-in JSONB field to store the extracted extra_data. # To enable it define the setting: # SOCIAL_AUTH_POSTGRES_JSONFIELD = True # Identifier of the SP entity (must be a URI) -SOCIAL_AUTH_SAML_SP_ENTITY_ID = "https://manhattan.local/accounts/metadata" -SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = """Spam, ham and eggs""" -SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = """Spam, sausages and bacon""" +SOCIAL_AUTH_SAML_SP_ENTITY_ID = 'https://manhattan.local/accounts/metadata' +SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = '''Spam, ham and eggs''' +SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = '''Spam, sausages and bacon''' SOCIAL_AUTH_SAML_ORG_INFO = { - "en-US": { - "name": "manhattan", - "displayname": "Proyectos de Innovación Docente", - "url": "http://manhattan.local", - } -} -SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = { - "givenName": "Quique", - "emailAddress": "quique@manhattan.local", + 'en-US': {'name': 'manhattan', 'displayname': 'Proyectos de Innovación Docente', 'url': 'http://manhattan.local'} } +SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {'givenName': 'Quique', 'emailAddress': 'quique@manhattan.local'} SOCIAL_AUTH_SAML_SUPPORT_CONTACT = { - "givenName": "Vicerrectorado de Política Académica", - "emailAddress": "innova.docen@manhattan.local", + 'givenName': 'Vicerrectorado de Política Académica', + 'emailAddress': 'innova.docen@manhattan.local', } # Si se cambia el backend de autenticación, actualizar clean() en InvitacionForm SOCIAL_AUTH_SAML_ENABLED_IDPS = { - "lord": { - "entity_id": "https://FIXME.idp.com/saml2/idp/metadata.php", - "url": "https://FIXME.idp.com/saml2/idp/SSOService.php", - "slo_url": "https://FIXME.idp.com/saml2/idp/SingleLogoutService.php", - "x509cert": "Lovely spam, wonderful spam", - "attr_user_permanent_id": "uid", - "attr_full_name": "cn", # "urn:oid:2.5.4.3" - "attr_first_name": "givenName", # "urn:oid:2.5.4.42" - "attr_last_name": "sn", # "urn:oid:2.5.4.4" - "attr_username": "uid", # "urn:oid:0.9.2342.19200300.100.1.1" + 'lord': { + 'entity_id': 'https://FIXME.idp.com/saml2/idp/metadata.php', + 'url': 'https://FIXME.idp.com/saml2/idp/SSOService.php', + 'slo_url': 'https://FIXME.idp.com/saml2/idp/SingleLogoutService.php', + 'x509cert': 'Lovely spam, wonderful spam', + 'attr_user_permanent_id': 'uid', + 'attr_full_name': 'cn', # "urn:oid:2.5.4.3" + 'attr_first_name': 'givenName', # "urn:oid:2.5.4.42" + 'attr_last_name': 'sn', # "urn:oid:2.5.4.4" + 'attr_username': 'uid', # "urn:oid:0.9.2342.19200300.100.1.1" # "attr_email": "email", # "urn:oid:0.9.2342.19200300.100.1.3" } } SOCIAL_AUTH_PIPELINE = ( - "social_core.pipeline.social_auth.social_details", - "social_core.pipeline.social_auth.social_uid", - "social_core.pipeline.social_auth.auth_allowed", - "social_core.pipeline.social_auth.social_user", - "social_core.pipeline.user.get_username", - "social_core.pipeline.user.create_user", + 'social_core.pipeline.social_auth.social_details', + 'social_core.pipeline.social_auth.social_uid', + 'social_core.pipeline.social_auth.auth_allowed', + 'social_core.pipeline.social_auth.social_user', + 'social_core.pipeline.user.get_username', + 'social_core.pipeline.user.create_user', # Actualizar con los datos de Gestión de Identidades - "accounts.pipeline.get_identidad", - "social_core.pipeline.social_auth.associate_user", - "social_core.pipeline.social_auth.load_extra_data", - "social_core.pipeline.user.user_details", + 'accounts.pipeline.get_identidad', + 'social_core.pipeline.social_auth.associate_user', + 'social_core.pipeline.social_auth.load_extra_data', + 'social_core.pipeline.user.user_details', ) -SOCIAL_AUTH_URL_NAMESPACE = "social" +SOCIAL_AUTH_URL_NAMESPACE = 'social' # CRISPY FORMS -CRISPY_TEMPLATE_PACK = "bootstrap4" +CRISPY_TEMPLATE_PACK = 'bootstrap4' # SUMMERNOTE -MEDIA_URL = "/media/" -MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -SUMMERNOTE_THEME = "bs4" +SUMMERNOTE_THEME = 'bs4' SUMMERNOTE_CONFIG = { # Using SummernoteWidget - iframe mode, default - "iframe": True, + 'iframe': True, # You can put custom Summernote settings - "summernote": { + 'summernote': { # As an example, using Summernote Air-mode # 'airMode': False, # Change editor size - "width": "100%", - "height": "480", + 'width': '100%', + 'height': '480', # Use proper language setting automatically (default) - "lang": None, + 'lang': None, # Or, set editor language/locale forcely # "lang": "ko-KR", # You can also add custom settings for external plugins # "print": { # "stylesheetUrl": "/some_static_folder/printable.css", # }, - "codemirror": { - "mode": "htmlmixed", - "lineNumbers": "true", + 'codemirror': { + 'mode': 'htmlmixed', + 'lineNumbers': 'true', # You have to include theme file in 'css' or 'css_for_inplace' # before using it. - "theme": "monokai", + 'theme': 'monokai', }, - "placeholder": "Introduzca sus comentarios", - "toolbar": [ + 'placeholder': 'Introduzca sus comentarios', + 'toolbar': [ # - ["style", ["style"]], - [ - "font", - ["bold", "italic", "forecolor", "superscript", "subscript", "clear"], - ], - ["para", ["ul", "ol", "paragraph"]], - ["table", ["table"]], - ["insert", ["link", "picture"]], - ["view", ["fullscreen", "codeview"]], + ['style', ['style']], + ['font', ['bold', 'italic', 'forecolor', 'superscript', 'subscript', 'clear']], + ['para', ['ul', 'ol', 'paragraph']], + ['table', ['table']], + ['insert', ['link', 'picture']], + ['view', ['fullscreen', 'codeview']], ], - "codeviewFilter": True, - "codeviewIframeFilter": True, + 'codeviewFilter': True, + 'codeviewIframeFilter': True, }, # Need authentication while uploading attachments. - "attachment_require_authentication": True, + 'attachment_require_authentication': True, # Set `upload_to` function for attachments. # 'attachment_upload_to': my_custom_upload_to_func(), # Set custom storage class for attachments. @@ -305,13 +286,11 @@ SUMMERNOTE_CONFIG = { # Codemirror as codeview # If any codemirror settings are defined, # it will include codemirror files automatically. - "css": ( - "//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/theme/monokai.min.css", - ), + 'css': ('//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/theme/monokai.min.css',), # Lazy initialize # If you want to initialize summernote at the bottom of page, set this as True # and call `initSummernote()` on your page. - "lazy": False, + 'lazy': False, # To use external plugins, # Include them within `css` and `js`. # 'js': { @@ -322,49 +301,49 @@ SUMMERNOTE_CONFIG = { # BLEACH ADDITIONAL_ALLOWED_TAGS = [ - "br", - "del", - "div", - "dl", - "dt", - "dd", - "em", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "img", - "ins", - "p", - "pre", - "span", - "strike", - "sub", - "sup", - "table", - "tbody", - "td", - "th", - "thead", - "tr", - "u", + 'br', + 'del', + 'div', + 'dl', + 'dt', + 'dd', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'img', + 'ins', + 'p', + 'pre', + 'span', + 'strike', + 'sub', + 'sup', + 'table', + 'tbody', + 'td', + 'th', + 'thead', + 'tr', + 'u', ] ALLOWED_ATTRIBUTES = { - "*": ["class", "id", "style"], - "a": ["alt", "href", "target", "title"], - "abbr": ["title"], - "acronym": ["title"], - "img": ["alt", "src"], + '*': ['class', 'id', 'style'], + 'a': ['alt', 'href', 'target', 'title'], + 'abbr': ['title'], + 'acronym': ['title'], + 'img': ['alt', 'src'], } -ALLOWED_STYLES = ["background-color", "color", "text-align"] -ALLOWED_PROTOCOLS = ["data", "http", "https", "mailto"] +ALLOWED_STYLES = ['background-color', 'color', 'text-align'] +ALLOWED_PROTOCOLS = ['data', 'http', 'https', 'mailto'] -X_FRAME_OPTIONS = "SAMEORIGIN" # Required by SummernoteWidget on Django 3.x +X_FRAME_OPTIONS = 'SAMEORIGIN' # Required by SummernoteWidget on Django 3.x # WEB SERVICE DE GESTIÓN DE IDENTIDADES -WSDL_IDENTIDAD = os.environ.get("WSDL_IDENTIDAD") -USER_IDENTIDAD = os.environ.get("USER_IDENTIDAD") -PASS_IDENTIDAD = os.environ.get("PASS_IDENTIDAD") +WSDL_IDENTIDAD = os.environ.get('WSDL_IDENTIDAD') +USER_IDENTIDAD = os.environ.get('USER_IDENTIDAD') +PASS_IDENTIDAD = os.environ.get('PASS_IDENTIDAD') diff --git a/manhattan_project/urls.py b/manhattan_project/urls.py index f8bbc9f..441215d 100644 --- a/manhattan_project/urls.py +++ b/manhattan_project/urls.py @@ -1,4 +1,4 @@ -"""manhattan_project URL Configuration +'''manhattan_project URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/2.1/topics/http/urls/ @@ -12,7 +12,7 @@ Class-based views Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" +''' from django.contrib import admin from django.contrib.staticfiles.storage import staticfiles_storage from django.urls import include, path @@ -21,24 +21,18 @@ from django.views.generic.base import RedirectView urlpatterns = [ - path("admin/", admin.site.urls), - path("accounts/", include("accounts.urls")), - path("accounts/", include("django.contrib.auth.urls")), + path('admin/', admin.site.urls), + path('accounts/', include('accounts.urls')), + path('accounts/', include('django.contrib.auth.urls')), path( - "browserconfig.xml", - RedirectView.as_view(url=staticfiles_storage.url("favicons/browserconfig.xml")), - name="browserconfig", + 'browserconfig.xml', + RedirectView.as_view(url=staticfiles_storage.url('favicons/browserconfig.xml')), + name='browserconfig', ), + path('favicon.ico', RedirectView.as_view(url=staticfiles_storage.url('favicons/favicon.ico')), name='favicon'), path( - "favicon.ico", - RedirectView.as_view(url=staticfiles_storage.url("favicons/favicon.ico")), - name="favicon", + 'robots.txt', TemplateView.as_view(template_name='robots.txt', content_type='text/plain'), name='robots_file' ), - path( - "robots.txt", - TemplateView.as_view(template_name="robots.txt", content_type="text/plain"), - name="robots_file", - ), - path("", include("social_django.urls", namespace="social")), - path("", include("indo.urls")), + path('', include('social_django.urls', namespace='social')), + path('', include('indo.urls')), ] diff --git a/manhattan_project/wsgi.py b/manhattan_project/wsgi.py index e0b7592..4bfd875 100644 --- a/manhattan_project/wsgi.py +++ b/manhattan_project/wsgi.py @@ -1,17 +1,17 @@ -""" +''' WSGI config for manhattan_project project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ -""" +''' import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manhattan_project.settings") +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'manhattan_project.settings') application = get_wsgi_application() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bdc02c4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.black] +skip-string-normalization = true +line-length = 119 +target-version = ['py37'] +include = '\.pyi?$' +exclude = ''' + +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + )/ + | foo.py # also separately exclude a file named foo.py in + # the root of the project +) +''' -- 2.18.0