diff --git a/.pep8 b/.pep8 new file mode 100644 index 0000000..2c0de05 --- /dev/null +++ b/.pep8 @@ -0,0 +1,3 @@ +[pycodestyle] +max_line_length = 120 +ignore = E501 \ No newline at end of file diff --git a/VRE/apps/api/exceptions.py b/VRE/apps/api/exceptions.py index 8d6f2f3..e2569bc 100644 --- a/VRE/apps/api/exceptions.py +++ b/VRE/apps/api/exceptions.py @@ -25,13 +25,16 @@ def json_schema_error_message(exc, context): # } # ] - system_errors = exc.get_full_details() + try: + system_errors = exc.get_full_details() + except Exception as ex: + system_errors = [{'exception' : {'code' : 'exception', 'message' : 'Something went wrong'}}] if isinstance(system_errors, list): - system_errors = {'header' : system_errors} + system_errors = {'_no_field' : system_errors} elif system_errors.get('message') is not None: # If there is a message keyword in the main system errors, then it is not a form error, but a more general error - system_errors = {'header' : [system_errors]} + system_errors = {'_no_field' : [system_errors]} json_errors = [] for errorfield, errors in system_errors.items(): diff --git a/VRE/apps/api/urls.py b/VRE/apps/api/urls.py index 58182fe..3700d7c 100644 --- a/VRE/apps/api/urls.py +++ b/VRE/apps/api/urls.py @@ -1,7 +1,5 @@ from django.urls import path, re_path, include - from rest_framework import permissions, routers - from drf_yasg.views import get_schema_view from drf_yasg import openapi @@ -12,25 +10,25 @@ from . import views # from apps.storage.views import StorageEngineViewSet, StorageLocationViewSet from apps.study.views import Studies from apps.virtual_machine.views import (VirtualMachineViewSet, - VirtualMachineOperatingSystemViewSet, - VirtualMachineProfileViewSet, - VirtualMachineMemoryViewSet, - VirtualMachineNetworkViewSet, - VirtualMachineStorageViewSet, - VirtualMachineGPUViewSet, - VirtualMachineAccessViewSet) + VirtualMachineOperatingSystemViewSet, + VirtualMachineProfileViewSet, + VirtualMachineMemoryViewSet, + VirtualMachineNetworkViewSet, + VirtualMachineStorageViewSet, + VirtualMachineGPUViewSet, + VirtualMachineAccessViewSet) schema_view = get_schema_view( - openapi.Info( - title="Virtual Research Environment API", - default_version='v1', - description="Here you can see a list of API endpoints and actions that are available to communicate with the VRE API", - terms_of_service="https://www.rug.nl", - contact=openapi.Contact(email="vre_team@rug.nl"), - license=openapi.License(name="MIT License"), - ), - public=True, - permission_classes=(permissions.AllowAny,), + openapi.Info( + title="Virtual Research Environment API", + default_version='v1', + description="Here you can see a list of API endpoints and actions that are available to communicate with the VRE API", + terms_of_service="https://www.rug.nl", + contact=openapi.Contact(email="vre_team@rug.nl"), + license=openapi.License(name="MIT License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), ) api_router_v1 = routers.DefaultRouter() @@ -60,20 +58,20 @@ api_router_v1.urls.append(path('vrw/', include('apps.vrw.urls'))) # Main namespace for the API urls app_name = 'api' urlpatterns = [ - re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), - path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), - # Authentication urls - path('auth/', include('djoser.urls')), - path('auth/', include('djoser.urls.jwt')), - # Custom login for getting the HAWK keys - path('auth/hawk/create/', views.Login.as_view(), name='api-login'), + # Authentication urls + path('auth/', include('djoser.urls')), + path('auth/', include('djoser.urls.jwt')), + # Custom login for getting the HAWK keys + path('auth/hawk/create/', views.Login.as_view(), name='api-login'), - # Extra /api/info path for checking if the Hawk authentication is working. - # Also this will give the full url to the OpenAPI documentation - path('info/', views.Info.as_view(), name='api-info'), + # Extra /api/info path for checking if the Hawk authentication is working. + # Also this will give the full url to the OpenAPI documentation + path('info/', views.Info.as_view(), name='api-info'), - # Add extra namespace for versioning the API - path('v1/', include((api_router_v1.urls,'v1'))), -] \ No newline at end of file + # Add extra namespace for versioning the API + path('v1/', include((api_router_v1.urls, 'v1'))), +] diff --git a/VRE/apps/study/__init__.py b/VRE/apps/study/__init__.py index d805ad2..9e934ba 100644 --- a/VRE/apps/study/__init__.py +++ b/VRE/apps/study/__init__.py @@ -1 +1 @@ -default_app_config = 'apps.study.apps.StudyConfig' \ No newline at end of file +default_app_config = 'apps.study.apps.StudyConfig' diff --git a/VRE/apps/study/admin.py b/VRE/apps/study/admin.py index 05f29ba..1282b87 100644 --- a/VRE/apps/study/admin.py +++ b/VRE/apps/study/admin.py @@ -7,18 +7,20 @@ from .models import Study, StudyRole # Register your models here. admin.site.register(StudyRole) + class role_inline(admin.TabularInline): model = StudyRole extra = 1 + @admin.register(Study) class StudyAdmin(admin.ModelAdmin): inlines = (role_inline,) - list_display = ('name', 'total_files', 'total_file_size', 'total_invitations', 'created_at') - ordering = ('-created_at','name', ) - search_fields = ('name', ) + list_display = ('name', 'total_files', 'total_file_size', 'total_invitations', 'created_at') + ordering = ('-created_at', 'name', ) + search_fields = ('name', ) readonly_fields = ('upload_uuid', 'api_upload_url', 'total_files', 'total_file_size', 'total_invitations', 'created_at', 'updated_at') - def total_file_size(self,obj): + def total_file_size(self, obj): return filesizeformat(obj.total_file_size) diff --git a/VRE/apps/study/apps.py b/VRE/apps/study/apps.py index 4698240..6d8649f 100644 --- a/VRE/apps/study/apps.py +++ b/VRE/apps/study/apps.py @@ -2,20 +2,21 @@ from django.apps import AppConfig from django.conf import settings from django.utils.translation import ugettext_lazy as _ + class StudyConfig(AppConfig): name = 'apps.study' label = 'study' verbose_name = _('Study') verbose_name_plural = _('Studies') - if not hasattr(settings,'DEFAULT_VM_PROFILE'): + if not hasattr(settings, 'DEFAULT_VM_PROFILE'): settings.DEFAULT_VM_PROFILE = 2 - if not hasattr(settings,'DEFAULT_VM_OS'): + if not hasattr(settings, 'DEFAULT_VM_OS'): settings.DEFAULT_VM_OS = 2 def ready(self): """ Load custom signals for creating :term:`VRW` models """ - import apps.study.signals \ No newline at end of file + import apps.study.signals diff --git a/VRE/apps/study/models.py b/VRE/apps/study/models.py index d9a53ca..7fb4119 100644 --- a/VRE/apps/study/models.py +++ b/VRE/apps/study/models.py @@ -13,6 +13,8 @@ from apps.university.models import StudyField import uuid # Create your models here. + + class StudyManager(models.Manager): """ This is a custom study manager for getting extra information from a study model like: @@ -31,25 +33,26 @@ class StudyManager(models.Manager): This will overrule/alter the existing queryset. """ return super().get_queryset().select_related('storagelocation').annotate( - _total_files=Count('files', distinct=True), - _total_file_size=Sum('files__filesize', distinct=True), - _total_invitations=Count('invitations', distinct=True), - ) + _total_files=Count('files', distinct=True), + _total_file_size=Sum('files__filesize', distinct=True), + _total_invitations=Count('invitations', distinct=True), + ) + class StudyRoleNames(models.TextChoices): """Research study role options Valid status values: - Administrator - - Owner - Researcher - Member """ - ADMIN = ('ADMIN', _('Administrator')) - OWNER = ('OWNER', _('Owner')) + ADMIN = ('ADMIN', _('Administrator')) +# OWNER = ('OWNER', _('Owner')) RESEARCHER = ('RESEARCHER', _('Researcher')) - MEMBER = ('MEMBER', _('Member')) + MEMBER = ('MEMBER', _('Member')) + class Study(MetaDataModel): """ @@ -75,35 +78,34 @@ class Study(MetaDataModel): verbose_name_plural = _('studies') ordering = ['name'] + owner = models.ForeignKey(Researcher, verbose_name=Researcher._meta.verbose_name, on_delete=models.CASCADE, help_text=_('The researcher that is the owner this study. By default the researcher that has created this study.')) + name = models.CharField(_('Name'), max_length=200, help_text=_('Name of the research study.')) + description = models.TextField(_('Description'), blank=True, null=True, help_text=_('Enter a short description for this study.')) - owner = models.ForeignKey(Researcher, verbose_name=Researcher._meta.verbose_name, on_delete=models.CASCADE, help_text=_('The researcher that is the owner this study. By default the researcher that has created this study.')) - name = models.CharField(_('Name'), max_length=200, help_text=_('Name of the research study.')) - description = models.TextField(_('Description'), blank=True, null=True, help_text=_('Enter a short description for this study.')) - - code = models.CharField(_('Code'), max_length=50, help_text=_('The research study code.')) - human_subject = models.BooleanField(_('Human subject'), help_text=_('Is this research study using real humans.')) - field = models.ForeignKey(StudyField, verbose_name=StudyField._meta.verbose_name, on_delete=models.CASCADE, help_text=_('The study field for this reaserch study.')) + code = models.CharField(_('Code'), max_length=50, help_text=_('The research study code.')) + human_subject = models.BooleanField(_('Human subject'), help_text=_('Is this research study using real humans.')) + field = models.ForeignKey(StudyField, verbose_name=StudyField._meta.verbose_name, on_delete=models.CASCADE, help_text=_('The study field for this reaserch study.')) - contributors = models.ManyToManyField(Researcher, related_name='contributors', through='StudyRole', through_fields=('study','researcher')) + contributors = models.ManyToManyField(Researcher, related_name='contributors', through='StudyRole', through_fields=('study', 'researcher')) # These are used for accepting files (datadrops) - upload_code = models.CharField(_('Upload code'), max_length=20, default=get_random_int_value, editable=False, help_text=_('A unique upload code. Will be generated when a new study is saved.')) - upload_uuid = models.UUIDField(_('Upload url key'), unique=True, default=uuid.uuid4, editable=False, help_text=_('A unique upload url. Will be generated when a new study is saved.')) + upload_code = models.CharField(_('Upload code'), max_length=20, default=get_random_int_value, editable=False, help_text=_('A unique upload code. Will be generated when a new study is saved.')) + upload_uuid = models.UUIDField(_('Upload url key'), unique=True, default=uuid.uuid4, editable=False, help_text=_('A unique upload url. Will be generated when a new study is saved.')) # Here we load our custom StudyManager so we always have the amount of files, total file size and amount of invitations directly accessable - objects = StudyManager() + objects = StudyManager() @property def has_storage(self): """boolean: Returns true when there is at least one storage location connected to this study""" - #print(dir(self.storagelocation)) + # print(dir(self.storagelocation)) return self.storagelocation is not None @property def get_absolute_url(self): """str: Returns the full url to the study detail page.""" return 'aaa' - #return reverse('study:detail', kwargs={'study_id': self.pk}) + # return reverse('study:detail', kwargs={'study_id': self.pk}) @property def web_upload_url(self): @@ -134,12 +136,13 @@ class Study(MetaDataModel): """str: Returns a readable string for the study.""" return f'{self.name}' + class StudyRole(MetaDataModel): - study = models.ForeignKey(Study, verbose_name=Study._meta.verbose_name, on_delete=models.CASCADE, help_text=_('Study')) + study = models.ForeignKey(Study, verbose_name=Study._meta.verbose_name, on_delete=models.CASCADE, help_text=_('Study')) researcher = models.ForeignKey(Researcher, verbose_name=Researcher._meta.verbose_name, on_delete=models.CASCADE, help_text=_('Researcher')) - role = models.CharField(_('Role'), max_length=10, choices=StudyRoleNames.choices, default=StudyRoleNames.MEMBER, help_text=_('The role withing this research study.')) - active = models.BooleanField(_('Active'), default=True, help_text=_('')) + role = models.CharField(_('Role'), max_length=10, choices=StudyRoleNames.choices, default=StudyRoleNames.MEMBER, help_text=_('The role withing this research study.')) + active = models.BooleanField(_('Active'), default=True, help_text=_('')) def __str__(self): """str: Returns a readable string for the study role.""" - return f'{self.researcher} is {self.role} of {self.study.name}' \ No newline at end of file + return f'{self.researcher} is {self.role} of {self.study.name}' diff --git a/VRE/apps/study/permissions.py b/VRE/apps/study/permissions.py index 8df6779..630a345 100644 --- a/VRE/apps/study/permissions.py +++ b/VRE/apps/study/permissions.py @@ -1,5 +1,6 @@ from rest_framework.permissions import BasePermission + class IsStudyContributor(BasePermission): def has_object_permission(self, request, view, obj): - return obj.contributors.filter(researcher=request.user.researcher).exists() \ No newline at end of file + return obj.contributors.filter(researcher=request.user.researcher).exists() diff --git a/VRE/apps/study/tests.py b/VRE/apps/study/tests.py index 2561085..5978412 100644 --- a/VRE/apps/study/tests.py +++ b/VRE/apps/study/tests.py @@ -14,24 +14,26 @@ from django.urls import reverse import random # Create your tests here. + + class StudyCreateTest(TestCase): - fixtures = ('university_initial_data','virtual_machine_initial_data') + fixtures = ('university_initial_data', 'virtual_machine_initial_data') @classmethod def setUpTestData(cls): cls.firstname = 'Jan' - cls.lastname = 'Doedel' - cls.email = 'dummy@rug.nl' - cls.username = 'dummy@rug.nl' - cls.password = ';t2QG*LLtzdnC*' + cls.lastname = 'Doedel' + cls.email = 'dummy@rug.nl' + cls.username = 'dummy@rug.nl' + cls.password = ';t2QG*LLtzdnC*' - cls.faculty = Faculty.objects.get(pk=9) + cls.faculty = Faculty.objects.get(pk=9) # Create a new user - cls.user = User.objects.create_user(username = cls.username, password = cls.password, email = cls.email) + cls.user = User.objects.create_user(username=cls.username, password=cls.password, email=cls.email) cls.user.first_name = cls.firstname - cls.user.last_name = cls.lastname + cls.user.last_name = cls.lastname cls.user.save() # Make a researcher member of a faculty @@ -42,7 +44,7 @@ class StudyCreateTest(TestCase): # Create some extra researchers..... for x in range(5): - user = User.objects.create_user(username = f'Username_{x}', password = f'{cls.password}_{x}', email = f'researcher_{x}@rug.nl') + user = User.objects.create_user(username=f'Username_{x}', password=f'{cls.password}_{x}', email=f'researcher_{x}@rug.nl') user.first_name = f'Rs_{x}' user.last_name = f'LN_{x}' user.save() @@ -52,7 +54,7 @@ class StudyCreateTest(TestCase): cls.valid_contributor = user else: - user.researcher.faculty = Faculty.objects.get(pk=random.randint(2,8)) + user.researcher.faculty = Faculty.objects.get(pk=random.randint(2, 8)) cls.invalid_contributor = user user.researcher.save() @@ -62,7 +64,7 @@ class StudyCreateTest(TestCase): self.client = RequestsClient() # Get a JWT token - login_data = {'username' : self.username, 'password' : self.password } + login_data = {'username': self.username, 'password': self.password} endpoint = 'http://testserver/api/auth/jwt/create/' response = self.client.post(endpoint, json=login_data) @@ -95,7 +97,7 @@ class StudyCreateTest(TestCase): # Check if we are the only reseacher.... self.assertEqual(len(study['contributors']), 1) # And make sure we are 'ADMIN' - self.assertEqual(study['contributors'][0]['role'],'ADMIN') + self.assertEqual(study['contributors'][0]['role'], 'ADMIN') self.assertEqual(study['contributors'][0]['researcher']['id'], self.user.researcher.id) # And now we should have 1 study @@ -121,7 +123,7 @@ class StudyCreateTest(TestCase): } response = self.client.post(endpoint, json=data) self.assertEqual(response.status_code, 400) - self.assertEqual(response.json()[0]['message'],f'Study field {study_field} is not valid for faculty {self.faculty}') + self.assertEqual(response.json()[0]['message'], f'Study field {study_field} is not valid for faculty {self.faculty}') def test_update_study(self): # We need a new study to test with.....:( @@ -130,7 +132,7 @@ class StudyCreateTest(TestCase): # Get the first study through Django Models, which should be the new created study study = Study.objects.get(contributors__in=[self.user.researcher]) - endpoint = 'http://testserver' + reverse('api:v1:study-detail',kwargs={ 'pk' : study.id}) + endpoint = 'http://testserver' + reverse('api:v1:study-detail', kwargs={'pk': study.id}) data = { "name": "Test Onderzoek - UPDATE", "description": "Doe maar een lange onderzoek - UPDATE", @@ -140,7 +142,7 @@ class StudyCreateTest(TestCase): } response = self.client.put(endpoint, json=data) - # And now we should have 1 study + # And now we should have 1 study endpoint = 'http://testserver' + reverse('api:v1:study-list') response = self.client.get(endpoint) @@ -149,7 +151,6 @@ class StudyCreateTest(TestCase): self.assertEqual(len(response.json()['results']), 1) self.assertEqual(response.json()['results'][0]['id'], study.id) - def test_add_new_contributor(self): # We need a new study to test with.....:( self.test_create_study() @@ -157,10 +158,10 @@ class StudyCreateTest(TestCase): # Get the first study through Django Models, which should be the new created study study = Study.objects.get(contributors__in=[self.user.researcher]) - endpoint = 'http://testserver' + reverse('api:v1:study-contributors', kwargs={ 'pk' : study.id}) + endpoint = 'http://testserver' + reverse('api:v1:study-contributors', kwargs={'pk': study.id}) data = [{ - "researcher_id" : self.valid_contributor.researcher.id, - "role" : "RESEARCHER" + "researcher_id": self.valid_contributor.researcher.id, + "role": "RESEARCHER" }] response = self.client.post(endpoint, json=data) @@ -176,12 +177,12 @@ class StudyCreateTest(TestCase): # Get the first study through Django Models, which should be the new created study study = Study.objects.get(contributors__in=[self.user.researcher]) - endpoint = 'http://testserver' + reverse('api:v1:study-contributors', kwargs={ 'pk' : study.id}) + endpoint = 'http://testserver' + reverse('api:v1:study-contributors', kwargs={'pk': study.id}) data = [{ - "researcher_id" : self.invalid_contributor.researcher.id, - "role" : "RESEARCHER" + "researcher_id": self.invalid_contributor.researcher.id, + "role": "RESEARCHER" }] response = self.client.post(endpoint, json=data) # We expect ONLY 1 contributors now. The invalid researcher should not be added - self.assertEqual(len(response.json()), 1) \ No newline at end of file + self.assertEqual(len(response.json()), 1) diff --git a/VRE/apps/university/models.py b/VRE/apps/university/models.py index 7f7e82b..e54627a 100644 --- a/VRE/apps/university/models.py +++ b/VRE/apps/university/models.py @@ -5,6 +5,8 @@ from django.utils.translation import gettext_lazy as _ from lib.models.base import MetaDataModel # Create your models here. + + class University(MetaDataModel): """ A model to represent a University. This model is used to combine faculties and their study fields. Also researchers are member of a university. @@ -25,8 +27,8 @@ class University(MetaDataModel): verbose_name_plural = _('universities') ordering = ['name'] - name = models.CharField(_('Name'), max_length=200, help_text=_('The name of the university.')) - email = models.EmailField(_('Email address'),max_length=200, help_text=_('The general email address for this university.')) + name = models.CharField(_('Name'), max_length=200, help_text=_('The name of the university.')) + email = models.EmailField(_('Email address'), max_length=200, help_text=_('The general email address for this university.')) website = models.CharField(_('Website'), max_length=200, help_text=_('The full url to this university website.')) def __str__(self): @@ -52,8 +54,8 @@ class Faculty(MetaDataModel): verbose_name_plural = _('faculties') ordering = ['name'] - name = models.CharField(_('Name'), max_length=200, help_text=_('The name of the faculty.')) - university = models.ForeignKey(University, verbose_name=University._meta.verbose_name, on_delete=models.CASCADE, help_text=_('To wich university belongs this faculty')) + name = models.CharField(_('Name'), max_length=200, help_text=_('The name of the faculty.')) + university = models.ForeignKey(University, verbose_name=University._meta.verbose_name, on_delete=models.CASCADE, help_text=_('To wich university belongs this faculty')) def __str__(self): """str: Returns a readable string for the facutlty.""" @@ -78,10 +80,9 @@ class StudyField(MetaDataModel): verbose_name_plural = _('study fields') ordering = ['name'] - name = models.CharField(_('Name'), max_length=200, help_text=_('The name of the study field.')) + name = models.CharField(_('Name'), max_length=200, help_text=_('The name of the study field.')) faculty = models.ForeignKey(Faculty, verbose_name=Faculty._meta.verbose_name, on_delete=models.CASCADE, help_text=_('To wich faculty belongs this study')) def __str__(self): """str: Returns a readable string for the studyfield.""" return f'{self.name}' -