Browse Source

Merge branch 'master' of https://git.web.rug.nl/VRE/Broker

master
Elwin Buisman 6 months ago
parent
commit
e7bff3217e
  1. 2
      .drone.yml
  2. 2
      .gitignore
  3. 9
      VRE/VRE/settings.py
  4. 5
      VRE/VRE/views.py
  5. 14
      VRE/apps/api/authentication.py
  6. 5
      VRE/apps/researcher/admin.py
  7. 74
      VRE/apps/researcher/management/commands/import_researchers.py
  8. 2
      VRE/apps/researcher/tests.py
  9. 5
      VRE/apps/researcher/views.py
  10. 7
      VRE/apps/study/admin.py
  11. 102
      VRE/apps/study/management/commands/import_studies.py
  12. 18
      VRE/apps/study/migrations/0012_alter_study_code.py
  13. 7
      VRE/apps/study/models.py
  14. 36
      VRE/apps/study/tests.py
  15. 2
      VRE/apps/study/urls.py
  16. 11
      VRE/apps/study/views.py
  17. 190
      VRE/apps/university/fixtures/university_initial_data.json
  18. 24
      VRE/apps/university/migrations/0002_auto_20211124_1149.py
  19. 18
      VRE/apps/university/migrations/0003_faculty_code.py
  20. 5
      VRE/apps/university/models.py
  21. 2
      VRE/apps/virtual_machine/providers/vrw/serializers.py
  22. 8
      VRE/apps/vre_apps/signals.py
  23. 2
      VRE/apps/vre_apps/views.py
  24. 46
      VRE/lib/models/admin.py
  25. 39
      VRE/templates/admin/import_change_list.html
  26. 21
      VRE/templates/admin/json_form.html
  27. 4
      doc/VRW.rst

2
.drone.yml

@ -46,7 +46,7 @@ steps: @@ -46,7 +46,7 @@ steps:
# Without this, we will hit a rate limit of Docker. This only works on the RUG Drones building setup
build_args:
- DOCKER_CACHE=registry.webhosting.rug.nl/cache/library/
- DEBUG=True
- DEBUG=False
tags:
- ${DRONE_SOURCE_BRANCH/\//-}
- ${DRONE_SOURCE_BRANCH/\//-}-${DRONE_COMMIT_SHA:0:8}

2
.gitignore vendored

@ -143,7 +143,7 @@ dmypy.json @@ -143,7 +143,7 @@ dmypy.json
cython_debug/
VRE/db.sqlite3*
VRE/media/*
.idea
docker/project.env
VRE/surfnet_conext_secrets.ini
media/

9
VRE/VRE/settings.py

@ -249,7 +249,7 @@ LOGIN_REDIRECT_URL = 'http://localhost:3000/' @@ -249,7 +249,7 @@ LOGIN_REDIRECT_URL = 'http://localhost:3000/'
LOGOUT_REDIRECT_URL = '/'
# Email settings for sending out upload invitations.
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='Do not reply<no-reply@rug.nl>')
EMAIL_HOST = config('EMAIL_HOST', default='')
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
@ -267,7 +267,7 @@ if DEBUG: @@ -267,7 +267,7 @@ if DEBUG:
# Study settings
# Duration of the JWT invite link in seconds. Default is 3 days
STUDY_INVITATION_LINK_EXPIRE_DURATION = config('STUDY_INVITATION_LINK_EXPIRE_DURATION', default=3 * 24 * 60 * 60, cast=int)
STUDY_INVITATION_LINK_DOMAIN = config('STUDY_INVITATION_LINK_DOMAIN', default='http://localhost:8000')
STUDY_INVITATION_LINK_DOMAIN = config('STUDY_INVITATION_LINK_DOMAIN', default='http://localhost:3000/researchStudies/{study_id}/contributors/join?key={jwt_token}')
# Dropoff settings.
# Enter the full path to the Webbased file uploading without the Study ID part. The Study ID will be added to this url based on the visitor.
@ -288,6 +288,9 @@ ACCESS_CONTROL_ALLOW_CREDENTIALS = True @@ -288,6 +288,9 @@ ACCESS_CONTROL_ALLOW_CREDENTIALS = True
# 'Access-Control-Allow-Credentials: true',
# ]
# Needed in order to make cross-site HTTP requests with authentication
CORS_ALLOW_CREDENTIALS = True
# Sentry settings
SENTRY_DSN = config('SENTRY_DSN', None)
if SENTRY_DSN:
@ -305,7 +308,7 @@ if SENTRY_DSN: @@ -305,7 +308,7 @@ if SENTRY_DSN:
send_default_pii=True
)
#Todo: Logging config
# Todo: Logging config
# LOGGING = {
# 'version': 1,

5
VRE/VRE/views.py

@ -1,9 +1,14 @@ @@ -1,9 +1,14 @@
from django.http import HttpResponse
from django.template import Template
from django.template.context import RequestContext
from django.shortcuts import redirect
def test_login_page(request):
# TODO: We could make some logic here to return to front-end...?
# if (request.user.is_authenticated):
# return redirect('http://localhost:3000/en/researchStudies')
template_string = '''<h1>Login test</h1>{% if user.is_authenticated %}
<p>Current user: {{ user.email }}</p>
<p><a href="{% url 'api:api-info' %}">API Status info</a></p>

14
VRE/apps/api/authentication.py

@ -96,9 +96,10 @@ class VRE_OIDC_Researcher_Update(OIDCAuthenticationBackend): @@ -96,9 +96,10 @@ class VRE_OIDC_Researcher_Update(OIDCAuthenticationBackend):
# We cannot handle multiple faculties for now
email_domain = claims.get('schac_home_organization', '')
faculty_list = claims.get('ou', [])
faculty_code = claims.get('ou', [])
faculty_code = None if len(faculty_code) == 0 else faculty_code[0].split(',')[-1].strip() # The format is: [Department], [Faculty_Code]
if '' == email_domain or len(faculty_list) == 0:
if '' == email_domain or faculty_code is None:
return None
university = University.objects.filter(email__endswith=f'@{email_domain}').first()
@ -107,15 +108,20 @@ class VRE_OIDC_Researcher_Update(OIDCAuthenticationBackend): @@ -107,15 +108,20 @@ class VRE_OIDC_Researcher_Update(OIDCAuthenticationBackend):
return None
# Find the first available faculty based on the provided list and university
faculty = Faculty.objects.filter(name__in=faculty_list, university=university).first()
faculty = Faculty.objects.filter(code=faculty_code, university=university).first()
if faculty is None:
# Create a new faculty, as we do not know it.
faculty = Faculty.objects.create(name=faculty_list[0], university=university)
faculty = Faculty.objects.create(code=faculty_code, name=f'Unknown faculty: {faculty}', university=university)
return faculty
def __enhance_user(self, user, claims):
update_user = False
# Update user id based on university email address
if (claims.get('email', '') != '' and claims.get('email') != user.username):
user.username = claims.get('email')
update_user = True
# Update first name
if (claims.get('given_name', '') != '' and claims.get('given_name') != user.first_name):
user.first_name = claims.get('given_name')

5
VRE/apps/researcher/admin.py

@ -1,14 +1,17 @@ @@ -1,14 +1,17 @@
from django.contrib import admin
from .models import Researcher
from lib.models.admin import JSONImportMixin
@admin.register(Researcher)
class ResearcherAdmin(admin.ModelAdmin):
class ResearcherAdmin(JSONImportMixin, admin.ModelAdmin):
list_display = ('user', 'last_name', 'faculty')
ordering = ('user', )
search_fields = ('user__username', 'user__first_name', 'user__last_name')
readonly_fields = ('created_at', 'updated_at', 'last_name')
change_list_template = 'admin/import_change_list.html'
import_type = 'researcher'
def last_name(self, obj):
return obj.user.last_name

74
VRE/apps/researcher/management/commands/import_researchers.py

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
from django.core.management.base import BaseCommand, CommandError, no_translations
from django.contrib.auth.models import User
import json
from pathlib import Path
class Command(BaseCommand):
help = '''Import researchers from external data source
Valid JSON schema is:
{
'email_address' :'',
'first_name': '',
'last_name' : '',
'mobile': '',
'pmunber' :''
}
'''
def add_arguments(self, parser):
parser.add_argument('import_file', help='JSON Import file with researchers')
@no_translations
def handle(self, *args, **options):
if options.get('import_file'):
import_file = Path(options.get("import_file"))
if not import_file.exists():
raise CommandError('Import file "%s" does not exists' % (import_file,))
try:
researchers_list = json.loads(import_file.read_text())
except json.JSONDecodeError:
raise CommandError('Could not read file "%s" data is not a valid JSON file' % (import_file,))
self.stdout.write(f'Start importing file: {import_file}')
import_counter = 0
for researcher_data in researchers_list:
if 'email_address' not in researcher_data or 'first_name' not in researcher_data or 'last_name' not in researcher_data:
self.stdout.write(self.style.ERROR('Skip import line due to wrong data...'))
continue
user, created = User.objects.get_or_create(email=researcher_data['email_address'],
defaults={
'username': researcher_data['email_address'],
'first_name': researcher_data['first_name'],
'last_name': researcher_data['last_name'],
'is_active': True}
)
if created:
user.set_unusable_password()
user.save()
update_researcher = False
if '' == user.researcher.mobilephone and 'mobile' in researcher_data and '' != researcher_data['mobile']:
user.researcher.mobilephone = researcher_data['mobile']
update_researcher = True
if '' == user.researcher.idnumber and 'pnumber' in researcher_data and '' != researcher_data['pnumber']:
user.researcher.idnumber = researcher_data['pnumber']
update_researcher = True
if update_researcher:
user.researcher.save()
import_counter += 1
self.stdout.write(self.style.SUCCESS(f'Researcher {user.researcher.display_name} {"is created" if created else "does already exists"}'))
self.stdout.write(f'Done importing file: {import_file}, {import_counter} entries ok!')

2
VRE/apps/researcher/tests.py

@ -22,7 +22,7 @@ class ResearcherTest(TestCase): @@ -22,7 +22,7 @@ class ResearcherTest(TestCase):
cls.username = 'dummy@rug.nl'
cls.password = ';t2QG*LLtzdnC*'
cls.faculty = Faculty.objects.get(pk=9)
cls.faculty = Faculty.objects.get(pk=13)
# Create a new user
cls.user = User.objects.create_user(username=cls.username, password=cls.password, email=cls.email)

5
VRE/apps/researcher/views.py

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_auto_schema
from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from .serializers import ResearcherSerializer, ResearcherDetailSerializer
@ -31,6 +33,9 @@ class Researchers(viewsets.ModelViewSet): @@ -31,6 +33,9 @@ class Researchers(viewsets.ModelViewSet):
@swagger_auto_schema(responses={200: ResearcherDetailSerializer(many=False)})
def me(self, request):
if not hasattr(request.user,'researcher'):
raise PermissionDenied(_('Illegal user'))
return Response(ResearcherDetailSerializer(request.user.researcher).data)
@swagger_auto_schema(request_body=ResearcherSerializer(many=False), responses={200: ResearcherDetailSerializer(many=False)})

7
VRE/apps/study/admin.py

@ -2,6 +2,8 @@ from django.contrib import admin @@ -2,6 +2,8 @@ from django.contrib import admin
from django.template.defaultfilters import filesizeformat
from .models import Study, StudyRole
from lib.models.admin import JSONImportMixin
# Register your models here.
admin.site.register(StudyRole)
@ -13,7 +15,7 @@ class role_inline(admin.TabularInline): @@ -13,7 +15,7 @@ class role_inline(admin.TabularInline):
@admin.register(Study)
class StudyAdmin(admin.ModelAdmin):
class StudyAdmin(JSONImportMixin, admin.ModelAdmin):
inlines = (role_inline,)
list_display = ('name', 'total_files', 'total_file_size', 'total_invitations', 'created_at')
@ -21,5 +23,8 @@ class StudyAdmin(admin.ModelAdmin): @@ -21,5 +23,8 @@ class StudyAdmin(admin.ModelAdmin):
search_fields = ('name', )
readonly_fields = ('upload_uuid', 'api_upload_url', 'total_files', 'total_file_size', 'total_invitations', 'created_at', 'updated_at')
change_list_template = 'admin/import_change_list.html'
import_type = 'study'
def total_file_size(self, obj):
return filesizeformat(obj.total_file_size)

102
VRE/apps/study/management/commands/import_studies.py

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
from django.core.management.base import BaseCommand, CommandError, no_translations
from django.contrib.auth.models import User
from apps.researcher.models import Researcher
from apps.study.models import Study, StudyRole
import json
from pathlib import Path
from apps.vre_apps.models import VRE_App, VRE_AppProfile
class Command(BaseCommand):
help = '''Import studies from external data source
Valid JSON schema is:
{
"name": "",
"code": "",
"description": "",
"contributors": [
{
"uid": "",
"role": "",
"workspace": ""
}
]
},
'''
def add_arguments(self, parser):
parser.add_argument('import_file', help='JSON Import file with researchers')
@no_translations
def handle(self, *args, **options):
if options.get('import_file'):
import_file = Path(options.get("import_file"))
if not import_file.exists():
raise CommandError('Import file "%s" does not exists' % (import_file,))
self.stdout.write(f'Start importing file: {import_file}')
try:
studies_list = json.loads(import_file.read_text())
except json.JSONDecodeError:
raise CommandError('Could not read file "%s" data is not a valid JSON file' % (import_file,))
import_counter = 0
for study_data in studies_list:
if 'name' not in study_data or 'description' not in study_data or 'code' not in study_data or 'contributors' not in study_data:
self.stdout.write(self.style.ERROR('Skip import line due to wrong data...'))
continue
# Determen the owner.... For now, the first admin role will do
Owner = None
for role in study_data['contributors']:
if 'Administrator' == role['role']:
Owner = User.objects.get(email=role['uid']).researcher
if Owner is None:
self.stdout.write(self.style.ERROR(f'Study {study.name} cannot be created as there is no owner defined (Administrator)'))
continue
# TODO: Or do we match on study code field??? Not sure if that data is available and correct in the LDAP
study, created = Study.objects.get_or_create(name=study_data['name'],
defaults={
'description': study_data['description'],
'code': study_data['code'],
'human_subject': False,
'field_id': 1,
'owner_id': Owner.pk}
)
# Add the contributors to the study
for role in study_data['contributors']:
try:
researcher = Researcher.objects.get(user__email=role['uid'])
except Researcher.DoesNotExist:
# The researcher is not imported first, so we have to ignore the role here
continue
contributor, created = StudyRole.objects.get_or_create(study=study, researcher=researcher,
defaults={
'role': role['role'].upper(),
'active': True
})
self.stdout.write(self.style.SUCCESS(f'New contributor {contributor.researcher.display_name} to study {study.name} {"is added" if created else "already exists"}.'))
# Create the workspace also here (vre_app)
try:
version = VRE_AppProfile.objects.get(app__slug='windows10-vdi', title__iexact=role['workspace'])
workspace, created = VRE_App.objects.get_or_create(contributor=contributor, version=version)
self.stdout.write(self.style.SUCCESS(f'New workspace {workspace} for study {study.name} is added.'))
except VRE_AppProfile.DoesNotExist:
self.stdout.write(self.style.ERROR(f'Could not create VRE App with type: {role["workspace"]}'))
import_counter += 1
self.stdout.write(self.style.SUCCESS(f'Study {study.name} {"is created" if created else "does already exists"}'))
self.stdout.write(f'Done importing file: {import_file}, {import_counter} entries')

18
VRE/apps/study/migrations/0012_alter_study_code.py

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
# Generated by Django 3.2.9 on 2021-11-30 09:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('study', '0011_auto_20211027_1320'),
]
operations = [
migrations.AlterField(
model_name='study',
name='code',
field=models.CharField(help_text='The research study code.', max_length=50, unique=True, verbose_name='Code'),
),
]

7
VRE/apps/study/models.py

@ -89,7 +89,7 @@ class Study(MetaDataModel): @@ -89,7 +89,7 @@ class Study(MetaDataModel):
avatar = models.ImageField(_('Avatar'), upload_to=file_upload_to, blank=True, null=True)
code = models.CharField(_('Code'), max_length=50, help_text=_('The research study code.'))
code = models.CharField(_('Code'),unique=True, 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.'))
@ -172,7 +172,7 @@ class StudyRole(MetaDataModel): @@ -172,7 +172,7 @@ class StudyRole(MetaDataModel):
}
jwt_token = jwt.encode(payload=jwt_data, key=settings.SECRET_KEY, algorithm="HS256")
url = f'{settings.STUDY_INVITATION_LINK_DOMAIN}' + reverse('api:v1:study-invite-join', kwargs={'study_id': self.study.pk, 'jwt_token': jwt_token})
url = settings.STUDY_INVITATION_LINK_DOMAIN.format(study_id=self.study.pk, jwt_token=jwt_token)
template_variables = {
'researcher': self.researcher.display_name,
@ -218,7 +218,8 @@ class StudyRole(MetaDataModel): @@ -218,7 +218,8 @@ class StudyRole(MetaDataModel):
jwt_data = None
try:
jwt_data = jwt.decode(jwt_token, settings.SECRET_KEY, algorithms=["HS256"])
except jwt.DecodeError:
raise ValidationError(_('Invitation token is not a valid JSON token'))
except jwt.ExpiredSignatureError:
raise ValidationError(_('Invitation token is expired. Please ask for a new invitation'))

36
VRE/apps/study/tests.py

@ -22,7 +22,7 @@ class StudyCreateTest(TestCase): @@ -22,7 +22,7 @@ class StudyCreateTest(TestCase):
cls.username = 'dummy@rug.nl'
cls.password = ';t2QG*LLtzdnC*'
cls.faculty = Faculty.objects.get(pk=9)
cls.faculty = Faculty.objects.get(pk=13)
# Create a new user
cls.user = User.objects.create_user(username=cls.username, password=cls.password, email=cls.email)
@ -107,6 +107,40 @@ class StudyCreateTest(TestCase): @@ -107,6 +107,40 @@ class StudyCreateTest(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()['results']), 1)
def test_create_study_unique_code(self):
# First time it should be ok
self.test_create_study()
# Now we create a new study with the same code value
# Need a study field based on faculty
study_field = StudyField.objects.filter(faculty=self.faculty).first()
endpoint = 'http://testserver' + reverse('api:v1:study-list')
data = {
"name": "Test Onderzoek",
"description": "Doe maar een lange onderzoek",
"code": "12345",
"human_subject": False,
"field": study_field.id
}
response = self.client.post(endpoint, json=data)
# Make sure we get a 201 result back
self.assertEqual(response.status_code, 400)
error_message = response.json()
self.assertEqual(error_message[0]['keyword'], 'unique')
# And now we should have still 1 study
endpoint = 'http://testserver' + reverse('api:v1:study-list')
response = self.client.get(endpoint)
# Check if we have 1 study
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()['results']), 1)
def test_create_study_with_png_image(self):
endpoint = 'http://testserver' + reverse('api:v1:study-list')
response = self.client.get(endpoint)

2
VRE/apps/study/urls.py

@ -4,7 +4,7 @@ from .views import Contributors, process_study_invite, validate_study_invite @@ -4,7 +4,7 @@ from .views import Contributors, process_study_invite, validate_study_invite
urlpatterns = [
path('<int:study_id>/contributors/', Contributors.as_view({'get': 'list'}), name='study-contributors'),
path('<int:study_id>/contributors/<int:contributor_id>/', Contributors.as_view({'get': 'get', 'put': 'update', 'post': 'update', 'delete': 'delete'}), name='study-contributors'),
path('<int:study_id>/contributors/<int:contributor_id>/', Contributors.as_view({'get': 'get', 'put': 'update', 'delete': 'delete'}), name='study-contributors'),
path('<int:study_id>/contributors/invite/', process_study_invite, name='study-invite'),
path('<int:study_id>/contributors/join/<path:jwt_token>', validate_study_invite, name='study-invite-join'),

11
VRE/apps/study/views.py

@ -43,6 +43,9 @@ class Studies(ModelViewSet): @@ -43,6 +43,9 @@ class Studies(ModelViewSet):
if getattr(self, 'swagger_fake_view', False):
return self.queryset
if not hasattr(self.request.user,'researcher'):
raise PermissionDenied(_('Illegal user'))
return Study.objects.filter(contributors__in=[self.request.user.researcher]).order_by('name')
def _valid_study_field_check(self, serializer):
@ -131,7 +134,7 @@ class Studies(ModelViewSet): @@ -131,7 +134,7 @@ class Studies(ModelViewSet):
@swagger_auto_schema(responses={200: StudyFieldSerializer(many=True)})
@action(detail=False, methods=['get'])
def fields(self, request):
"""Get the logged in researcher his study fields based on the faculty where he belongs to. In other words, this is the list of study fields where the logged in user can do research on.
"""Get the logged in researcher his study fields based on the faculty where he belongs to. In other words, this is the list of study fields where the logged in user can do research on. The list is empty when there is no faculty known to the researcher
Args:
request ([type]): The incoming web request
@ -139,7 +142,9 @@ class Studies(ModelViewSet): @@ -139,7 +142,9 @@ class Studies(ModelViewSet):
Returns:
StudyFieldSerializer: A list with zero or more study fields for the logged in researcher.
"""
study_fields = request.user.researcher.faculty.studyfield_set.all().order_by('name')
study_fields = []
if request.user.researcher.faculty:
study_fields = request.user.researcher.faculty.studyfield_set.all().order_by('name')
page = self.paginate_queryset(study_fields)
if page is not None:
@ -322,7 +327,7 @@ def validate_study_invite(request, *args, **kwargs): @@ -322,7 +327,7 @@ def validate_study_invite(request, *args, **kwargs):
JSON message: Message if ok
"""
# Check if we are a contributor of the project and does project exists. And make sure the user is not yet activated
study = get_object_or_404(Study, pk=kwargs['pk'], contributors__in=[request.user.researcher], studyrole__active=False)
study = get_object_or_404(Study, pk=kwargs['study_id'], contributors__in=[request.user.researcher], studyrole__active=False)
# Check if we are invited for this study
invitation = get_object_or_404(StudyRole, study=study, researcher=request.user.researcher, active=False)

190
VRE/apps/university/fixtures/university_initial_data.json

@ -15,8 +15,9 @@ @@ -15,8 +15,9 @@
"pk": 2,
"fields": {
"created_at": "2021-04-29T09:40:00.843Z",
"updated_at": "2021-04-29T09:40:00.843Z",
"name": "Economie en Bedrijfskunde",
"updated_at": "2021-12-03T09:19:34.697Z",
"code": "FEB",
"name": "Faculteit Economie en Bedrijfskunde",
"university": 1
}
},
@ -25,8 +26,9 @@ @@ -25,8 +26,9 @@
"pk": 3,
"fields": {
"created_at": "2021-04-29T09:40:15.232Z",
"updated_at": "2021-04-29T09:59:05.168Z",
"name": "Gedrags- en Maatschappij-wetenschappen",
"updated_at": "2021-12-03T09:21:47.259Z",
"code": "GMW",
"name": "Faculteit Gedrags- en Maatschappijwetenschappen",
"university": 1
}
},
@ -35,8 +37,9 @@ @@ -35,8 +37,9 @@
"pk": 4,
"fields": {
"created_at": "2021-04-29T09:40:26.287Z",
"updated_at": "2021-04-29T09:40:26.287Z",
"name": "Godgeleerdheid en Godsdienst- wetenschap",
"updated_at": "2021-12-03T09:21:19.441Z",
"code": "GGW",
"name": "Faculteit Godgeleerdheid en Godsdienstwetenschap",
"university": 1
}
},
@ -45,8 +48,9 @@ @@ -45,8 +48,9 @@
"pk": 5,
"fields": {
"created_at": "2021-04-29T09:40:34.920Z",
"updated_at": "2021-04-29T09:40:34.920Z",
"name": "Letteren",
"updated_at": "2021-12-03T09:22:16.377Z",
"code": "LET",
"name": "Faculteit der Letteren",
"university": 1
}
},
@ -55,8 +59,9 @@ @@ -55,8 +59,9 @@
"pk": 6,
"fields": {
"created_at": "2021-04-29T09:40:43.131Z",
"updated_at": "2021-04-29T09:40:43.131Z",
"name": "Medische Wetenschappen",
"updated_at": "2021-12-03T09:25:23.027Z",
"code": "UMC",
"name": "Faculteit Medische Wetenschappen (umcg)",
"university": 1
}
},
@ -65,8 +70,9 @@ @@ -65,8 +70,9 @@
"pk": 7,
"fields": {
"created_at": "2021-04-29T09:40:51.248Z",
"updated_at": "2021-04-29T10:04:42.753Z",
"name": "Rechtsgeleerdheid",
"updated_at": "2021-12-03T09:22:03.505Z",
"code": "JUR",
"name": "Faculteit Rechtsgeleerdheid",
"university": 1
}
},
@ -75,48 +81,130 @@ @@ -75,48 +81,130 @@
"pk": 8,
"fields": {
"created_at": "2021-04-29T09:41:00.072Z",
"updated_at": "2021-04-29T09:57:18.767Z",
"name": "Ruimtelijke Wetenschappen",
"updated_at": "2021-12-03T09:20:11.745Z",
"code": "FRW",
"name": "Faculteit Ruimtelijke Wetenschappen",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 9,
"pk": 10,
"fields": {
"created_at": "2021-04-29T09:41:10.777Z",
"updated_at": "2021-04-29T10:05:55.983Z",
"name": "Science and Engineering",
"created_at": "2021-04-29T09:41:17.643Z",
"updated_at": "2021-12-03T09:19:52.219Z",
"code": "FIL",
"name": "Faculteit Wijsbegeerte",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 10,
"pk": 11,
"fields": {
"created_at": "2021-04-29T09:41:17.643Z",
"updated_at": "2021-04-29T10:07:00.916Z",
"name": "Wijsbegeerte",
"created_at": "2021-04-29T09:41:24.270Z",
"updated_at": "2021-12-03T09:24:48.670Z",
"code": "UCG",
"name": "University College Groningen",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 11,
"pk": 13,
"fields": {
"created_at": "2021-04-29T09:41:24.270Z",
"updated_at": "2021-04-29T09:41:24.270Z",
"name": "University College Groningen",
"created_at": "2021-11-18T10:28:34.929Z",
"updated_at": "2021-12-03T09:18:25.325Z",
"code": "CIT",
"name": "Centrum voor Informatie Technologie",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 12,
"pk": 14,
"fields": {
"created_at": "2021-12-03T09:19:02.363Z",
"updated_at": "2021-12-03T09:19:02.363Z",
"code": "BUR",
"name": "Bureau van de Universiteit",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 15,
"fields": {
"created_at": "2021-12-03T09:19:19.215Z",
"updated_at": "2021-12-03T09:19:19.215Z",
"code": "CVB",
"name": "College van Bestuur",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 16,
"fields": {
"created_at": "2021-12-03T09:20:54.557Z",
"updated_at": "2021-12-03T09:20:54.557Z",
"code": "FWN",
"name": "Faculteit Wis- en Natuurkunde",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 17,
"fields": {
"created_at": "2021-12-03T09:23:29.654Z",
"updated_at": "2021-12-03T09:23:29.654Z",
"code": "PPO",
"name": "Postmaster Psychologie en Orthopedagogiek (gelieerde instelling)",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 18,
"fields": {
"created_at": "2021-12-03T09:23:50.750Z",
"updated_at": "2021-12-03T09:23:50.750Z",
"code": "PTH",
"name": "Protestants Theologische Universiteit (gelieerde instelling)",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 19,
"fields": {
"created_at": "2021-12-03T09:24:04.737Z",
"updated_at": "2021-12-03T09:24:04.737Z",
"code": "SRN",
"name": "SRON (gelieerde instelling)",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 20,
"fields": {
"created_at": "2021-12-03T09:24:31.917Z",
"updated_at": "2021-12-03T09:24:31.917Z",
"code": "UBG",
"name": "Universiteitsbibliotheek Groningen",
"university": 1
}
},
{
"model": "university.faculty",
"pk": 21,
"fields": {
"created_at": "2021-04-29T09:41:32.065Z",
"updated_at": "2021-04-29T09:41:32.066Z",
"name": "Rijksuniversiteit Groningen/Campus Fryslân",
"created_at": "2021-12-03T09:25:03.871Z",
"updated_at": "2021-12-03T09:25:03.871Z",
"code": "UFB",
"name": "Universitair Facilitair Bedrijf",
"university": 1
}
},
@ -325,9 +413,9 @@ @@ -325,9 +413,9 @@
"pk": 21,
"fields": {
"created_at": "2021-04-29T10:03:35.592Z",
"updated_at": "2021-04-29T10:03:35.592Z",
"updated_at": "2021-11-18T10:28:53.812Z",
"name": "Effective Criminal Law",
"faculty": 7
"faculty": 13
}
},
{
@ -335,9 +423,9 @@ @@ -335,9 +423,9 @@
"pk": 22,
"fields": {
"created_at": "2021-04-29T10:03:44.191Z",
"updated_at": "2021-04-29T10:03:44.191Z",
"updated_at": "2021-11-18T10:29:02.132Z",
"name": "Law on Energy and Sustainability",
"faculty": 7
"faculty": 13
}
},
{
@ -395,39 +483,9 @@ @@ -395,39 +483,9 @@
"pk": 28,
"fields": {
"created_at": "2021-04-29T10:05:18.656Z",
"updated_at": "2021-04-29T10:05:18.656Z",
"updated_at": "2021-11-18T10:29:41.838Z",
"name": "Advanced Materials",
"faculty": 9
}
},
{
"model": "university.studyfield",
"pk": 29,
"fields": {
"created_at": "2021-04-29T10:05:25.139Z",
"updated_at": "2021-04-29T10:05:25.140Z",
"name": "Molecular Life and Health",
"faculty": 9
}
},
{
"model": "university.studyfield",
"pk": 30,
"fields": {
"created_at": "2021-04-29T10:05:31.161Z",
"updated_at": "2021-04-29T10:05:31.161Z",
"name": "Adaptive Life",
"faculty": 9
}
},
{
"model": "university.studyfield",
"pk": 31,
"fields": {
"created_at": "2021-04-29T10:05:37.997Z",
"updated_at": "2021-04-29T10:05:37.997Z",
"name": "Data Science and Systems Complexity",
"faculty": 9
"faculty": 13
}
},
{

24
VRE/apps/university/migrations/0002_auto_20211124_1149.py

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
# Generated by Django 3.2.9 on 2021-11-24 11:49
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('university', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='faculty',
name='university',
field=models.ForeignKey(help_text='To which university belongs this faculty', on_delete=django.db.models.deletion.CASCADE, to='university.university', verbose_name='university'),
),
migrations.AlterField(
model_name='studyfield',
name='faculty',
field=models.ForeignKey(help_text='To which faculty belongs this study', on_delete=django.db.models.deletion.CASCADE, to='university.faculty', verbose_name='faculty'),
),
]

18
VRE/apps/university/migrations/0003_faculty_code.py

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
# Generated by Django 3.2.9 on 2021-12-03 09:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('university', '0002_auto_20211124_1149'),
]
operations = [
migrations.AddField(
model_name='faculty',
name='code',
field=models.CharField(default='___', help_text='The code for the faculty.', max_length=3, verbose_name='code'),
),
]

5
VRE/apps/university/models.py

@ -54,8 +54,9 @@ class Faculty(MetaDataModel): @@ -54,8 +54,9 @@ class Faculty(MetaDataModel):
verbose_name_plural = _('faculties')
ordering = ['name']
code = models.CharField(_('code'), max_length=3, help_text=_('The code for the faculty.'), default='___')
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'))
university = models.ForeignKey(University, verbose_name=University._meta.verbose_name, on_delete=models.CASCADE, help_text=_('To which university belongs this faculty'))
def __str__(self):
"""str: Returns a readable string for the facutlty."""
@ -81,7 +82,7 @@ class StudyField(MetaDataModel): @@ -81,7 +82,7 @@ class StudyField(MetaDataModel):
ordering = ['name']
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'))
faculty = models.ForeignKey(Faculty, verbose_name=Faculty._meta.verbose_name, on_delete=models.CASCADE, help_text=_('To which faculty belongs this study'))
def __str__(self):
"""str: Returns a readable string for the studyfield."""

2
VRE/apps/virtual_machine/providers/vrw/serializers.py

@ -15,7 +15,7 @@ class WorkspaceStudySerializer(serializers.ModelSerializer): @@ -15,7 +15,7 @@ class WorkspaceStudySerializer(serializers.ModelSerializer):
class Meta:
model = Study
fields = ['id', 'name']
fields = ['id', 'name', 'code']
class WorkspaceResearcherSerializer(serializers.Serializer):

8
VRE/apps/vre_apps/signals.py

@ -19,9 +19,13 @@ def create_VRE_app(sender, instance, created, **kwargs): @@ -19,9 +19,13 @@ def create_VRE_app(sender, instance, created, **kwargs):
created (bool): Is it a create or update action
"""
# check if the current researcher does have enough rights and does not have already a virtual machine
if created and instance.contributor.role in [StudyRoleNames.ADMIN, StudyRoleNames.CONTRIBUTOR] and VirtualMachine.objects.filter(researcher=instance.contributor.researcher, study=instance.contributor.study).count() == 0:
# Create a new virtual machine on premium profile
if created and \
instance.contributor.role in [StudyRoleNames.ADMIN, StudyRoleNames.CONTRIBUTOR] and \
VirtualMachine.objects.filter(researcher=instance.contributor.researcher,
study=instance.contributor.study,
profile=instance.version.content_object).count() == 0:
# Create a new virtual machine
virtual_machine = VirtualMachine.objects.create(
name=f'VM of {instance.contributor.researcher.display_name} for study {instance.contributor.study.name}',
researcher=instance.contributor.researcher,

2
VRE/apps/vre_apps/views.py

@ -118,7 +118,7 @@ class ListInstalledAppsAndCreateApp(AppTypeProfileSerializerMixin, ModelViewSet) @@ -118,7 +118,7 @@ class ListInstalledAppsAndCreateApp(AppTypeProfileSerializerMixin, ModelViewSet)
"""
# Check if the study does exists and that the requesting user is at least an active member
study = get_object_or_404(Study, pk=kwargs['study_d'], contributors__in=[request.user.researcher], studyrole__active=True)
study = get_object_or_404(Study, pk=kwargs['study_id'], contributors__in=[request.user.researcher], studyrole__active=True)
# Check if the app type is a valid one.
get_object_or_404(VRE_AppType, slug=kwargs['app_type'])

46
VRE/lib/models/admin.py

@ -1,6 +1,13 @@ @@ -1,6 +1,13 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.management import call_command
from django.shortcuts import redirect, render
from django.utils.safestring import mark_safe
from django.urls import path
from django.utils.translation import gettext_lazy as _
from pathlib import Path
class VMRelatedSelectWidget(forms.Select):
@ -58,3 +65,42 @@ class VMRelatedSelectWidget(forms.Select): @@ -58,3 +65,42 @@ class VMRelatedSelectWidget(forms.Select):
html = super().render(name, value, attrs, renderer) + '<script>' + javascript_data + '</script>'
return mark_safe(html)
class JSONImportForm(forms.Form):
json_file = forms.FileField( label='JSON Import file', allow_empty_file=False, required=True, help_text=_('Select a valid JSON file for uploading'))
class JSONImportMixin():
def get_urls(self):
urls = super().get_urls()
my_urls = [
path('import-json/', self.import_json, name=f'{self.import_type}_import_json'),
]
return my_urls + urls
def import_json(self, request):
if request.method == "POST":
import_file = '/tmp/' + 'study' if 'study' == self.import_type else 'researcher' + '.upload.json'
import_file = Path(import_file)
if import_file.exists():
import_file.unlink()
with request.FILES["json_file"].open('r') as source:
with open(import_file, mode='ab') as destination:
for line in source:
destination.write(line)
import_cmd = 'import_studies' if 'study' == self.import_type else 'import_researchers'
call_command(import_cmd, import_file)
import_file.unlink()
self.message_user(request, 'Your JSON file has been imported. Check if the results are ok below.')
return redirect("..")
form = JSONImportForm()
payload = {"form": form}
return render(
request, "admin/json_form.html", payload
)

39
VRE/templates/admin/import_change_list.html

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
{% extends 'admin/change_list.html' %}
{% load i18n %}
{% block object-tools %}
{{ block.super }}
<style>
div#admin_json_import_popup {
position: absolute;
top: 150px;
right: 50px;
border: solid 1px black;
width: 600px;
background-color: white;
}
</style>
<script>
function showForm(url) {
window.django.jQuery.get(url.target.href, function (data, ok) {
let popup_div = window.django.jQuery('div#admin_json_import_popup');
if (popup_div.length == 0) {
popup_div = window.django.jQuery('<div>').attr('id', 'admin_json_import_popup').css('display', 'none');
window.django.jQuery('div#content').append(popup_div);
}
data = data.replace('<div id="container">', '<div id="container" style="display: inline">')
popup_div.html(data).show();
return false;
});
return false;
}
window.django.jQuery(function () {
let import_link = window.django.jQuery('<a>').attr('href', 'import-json/').addClass('addlink').text('{% translate "Import JSON" %}').on('click', showForm);
window.django.jQuery('#content-main ul.object-tools').append(
window.django.jQuery('<li>').append(import_link)
);
});
</script>
{% endblock %}

21
VRE/templates/admin/json_form.html

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static admin_modify %}
{% block content %}
<h2>JSON Import</h2>
<form action="import-json/" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div>
<fieldset class="module aligned ">
<div class="form-row">
{{ form.json_file.label_tag }}
{{ form.json_file }}
<div class="help">{{ form.json_file.help_text }}</div>
</div>
</fieldset>
<div class="submit-row">
<input type="submit" value="Upload JSON File" class="default" name="_save">
</div>
</div>
</form>
{% endblock %}

4
doc/VRW.rst

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
===
VRW
===
This part of the application is for integrating with the RUG Virtual Research Workspaces (:term:`VRW`). The app will provide a REST API interface for machine management. An external script or system can then retreive the information from the API about which workspaces to create and which to clean up.
This part of the application is for integrating with the RUG Virtual Research Workspaces (:term:`VRW`). The app will provide a REST API interface for machine management. An external script or system can then retrieve the information from the API about which workspaces to create and which to clean up.
-------------
Authorization
-------------
This part of the API is only accessible with accounts that are added to a special :term:`VRW` API group. By default this group is called 'vre-api' and be changed with the setting ':attr:`~settings.VRW_API_GROUP`'. This is done so that normal users are not able to change :term:`VRW` statusses.
This part of the API is only accessible with accounts that are added to a special :term:`VRW` API group. By default this group is called 'vre-api' and be changed with the setting ':attr:`~settings.VRW_API_GROUP`'. This is done so that normal users are not able to change :term:`VRW` statuses.
The authentication is done by the general REST API.
.. automodule:: apps.virtual_machine.providers.vrw.permissions

Loading…
Cancel
Save