From 6b53c0fad76f25ca731bdc2eeabe19701df66054 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 23 Jan 2025 10:38:26 -0500 Subject: [PATCH 001/129] Create database --- sasdata/fair_database/db.sqlite3 | 0 .../fair_database/fair_database/__init__.py | 0 sasdata/fair_database/fair_database/asgi.py | 16 +++ .../fair_database/fair_database/settings.py | 123 ++++++++++++++++++ sasdata/fair_database/fair_database/urls.py | 22 ++++ sasdata/fair_database/fair_database/wsgi.py | 16 +++ sasdata/fair_database/manage.py | 22 ++++ 7 files changed, 199 insertions(+) create mode 100644 sasdata/fair_database/db.sqlite3 create mode 100644 sasdata/fair_database/fair_database/__init__.py create mode 100644 sasdata/fair_database/fair_database/asgi.py create mode 100644 sasdata/fair_database/fair_database/settings.py create mode 100644 sasdata/fair_database/fair_database/urls.py create mode 100644 sasdata/fair_database/fair_database/wsgi.py create mode 100755 sasdata/fair_database/manage.py diff --git a/sasdata/fair_database/db.sqlite3 b/sasdata/fair_database/db.sqlite3 new file mode 100644 index 00000000..e69de29b diff --git a/sasdata/fair_database/fair_database/__init__.py b/sasdata/fair_database/fair_database/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sasdata/fair_database/fair_database/asgi.py b/sasdata/fair_database/fair_database/asgi.py new file mode 100644 index 00000000..f47618a3 --- /dev/null +++ b/sasdata/fair_database/fair_database/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for fair_database project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fair_database.settings') + +application = get_asgi_application() diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py new file mode 100644 index 00000000..2ff2160c --- /dev/null +++ b/sasdata/fair_database/fair_database/settings.py @@ -0,0 +1,123 @@ +""" +Django settings for fair_database project. + +Generated by 'django-admin startproject' using Django 5.1.5. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure--f-t5!pdhq&4)^&xenr^k0e8n%-h06jx9d0&2kft(!+1$xzig)' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +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', +] + +ROOT_URLCONF = 'fair_database.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + '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', + ], + }, + }, +] + +WSGI_APPLICATION = 'fair_database.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + '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/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py new file mode 100644 index 00000000..30dc3122 --- /dev/null +++ b/sasdata/fair_database/fair_database/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for fair_database project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +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.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/sasdata/fair_database/fair_database/wsgi.py b/sasdata/fair_database/fair_database/wsgi.py new file mode 100644 index 00000000..cb087086 --- /dev/null +++ b/sasdata/fair_database/fair_database/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for fair_database 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/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fair_database.settings') + +application = get_wsgi_application() diff --git a/sasdata/fair_database/manage.py b/sasdata/fair_database/manage.py new file mode 100755 index 00000000..c74d5f9c --- /dev/null +++ b/sasdata/fair_database/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fair_database.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?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() From b53e353bfc087f34fcbb2d5fe7f766f649e887eb Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 23 Jan 2025 12:50:03 -0500 Subject: [PATCH 002/129] Create data application in database --- sasdata/fair_database/data/__init__.py | 0 sasdata/fair_database/data/admin.py | 3 +++ sasdata/fair_database/data/apps.py | 6 ++++++ sasdata/fair_database/data/migrations/__init__.py | 0 sasdata/fair_database/data/models.py | 3 +++ sasdata/fair_database/data/tests.py | 3 +++ sasdata/fair_database/data/urls.py | 7 +++++++ sasdata/fair_database/data/views.py | 7 +++++++ sasdata/fair_database/fair_database/urls.py | 3 ++- 9 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 sasdata/fair_database/data/__init__.py create mode 100644 sasdata/fair_database/data/admin.py create mode 100644 sasdata/fair_database/data/apps.py create mode 100644 sasdata/fair_database/data/migrations/__init__.py create mode 100644 sasdata/fair_database/data/models.py create mode 100644 sasdata/fair_database/data/tests.py create mode 100644 sasdata/fair_database/data/urls.py create mode 100644 sasdata/fair_database/data/views.py diff --git a/sasdata/fair_database/data/__init__.py b/sasdata/fair_database/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sasdata/fair_database/data/admin.py b/sasdata/fair_database/data/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/sasdata/fair_database/data/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/sasdata/fair_database/data/apps.py b/sasdata/fair_database/data/apps.py new file mode 100644 index 00000000..f6b7ef7f --- /dev/null +++ b/sasdata/fair_database/data/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DataConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'data' diff --git a/sasdata/fair_database/data/migrations/__init__.py b/sasdata/fair_database/data/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/sasdata/fair_database/data/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/sasdata/fair_database/data/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/sasdata/fair_database/data/urls.py b/sasdata/fair_database/data/urls.py new file mode 100644 index 00000000..97ba2b8f --- /dev/null +++ b/sasdata/fair_database/data/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("list/", views.list_data, name="list public files"), +] \ No newline at end of file diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py new file mode 100644 index 00000000..08219af8 --- /dev/null +++ b/sasdata/fair_database/data/views.py @@ -0,0 +1,7 @@ +from django.shortcuts import render + +# Create your views here +from django.http import HttpResponse + +def list_data(request): + return HttpResponse("Hello World! This is going to display data later.") \ No newline at end of file diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py index 30dc3122..e223d9d8 100644 --- a/sasdata/fair_database/fair_database/urls.py +++ b/sasdata/fair_database/fair_database/urls.py @@ -15,8 +15,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ + path('data/', include("data.urls")), path('admin/', admin.site.urls), ] From a4e5c7ed7c1d7ceb74e7f234d7012d9d8db14c97 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 23 Jan 2025 13:31:55 -0500 Subject: [PATCH 003/129] Add Data model class from webfit --- .../data/migrations/0001_initial.py | 28 +++++++++++++++++++ sasdata/fair_database/data/models.py | 19 +++++++++++++ .../fair_database/fair_database/settings.py | 1 + 3 files changed, 48 insertions(+) create mode 100644 sasdata/fair_database/data/migrations/0001_initial.py diff --git a/sasdata/fair_database/data/migrations/0001_initial.py b/sasdata/fair_database/data/migrations/0001_initial.py new file mode 100644 index 00000000..1c7c9df3 --- /dev/null +++ b/sasdata/fair_database/data/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.5 on 2025-01-23 18:41 + +import django.core.files.storage +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Data', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file_name', models.CharField(blank=True, default=None, help_text='File name', max_length=200, null=True)), + ('file', models.FileField(default=None, help_text='This is a file', storage=django.core.files.storage.FileSystemStorage(), upload_to='uploaded_files')), + ('is_public', models.BooleanField(default=False, help_text='opt in to submit your data into example pool')), + ('current_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 71a83623..e902193c 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -1,3 +1,22 @@ from django.db import models +from django.contrib.auth.models import User +from django.core.files.storage import FileSystemStorage # Create your models here. +class Data(models.Model): + #username + current_user = models.ForeignKey(User, blank=True, + null=True, on_delete=models.CASCADE) + + #file name + file_name = models.CharField(max_length=200, default=None, + blank=True, null=True, help_text="File name") + + #imported data + #user can either import a file path or actual file + file = models.FileField(blank=False, default=None, help_text="This is a file", + upload_to="uploaded_files", storage=FileSystemStorage()) + + #is the data public? + is_public = models.BooleanField(default=False, + help_text= "opt in to submit your data into example pool") \ No newline at end of file diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 2ff2160c..42b46625 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -31,6 +31,7 @@ # Application definition INSTALLED_APPS = [ + 'data.apps.DataConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', From 39dbd12a447b98915ee53ea06f2880c194f6fc85 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 23 Jan 2025 13:49:58 -0500 Subject: [PATCH 004/129] Add sqlite file to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 771b86bf..a0720852 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ **/build /dist .mplconfig +**/db.sqlite3 # doc build /docs/sphinx-docs/build From af9531241ffebac5bf24b4b9fb48628ed709be01 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 23 Jan 2025 14:30:32 -0500 Subject: [PATCH 005/129] Create urls for database. --- sasdata/fair_database/data/urls.py | 8 +++++++- sasdata/fair_database/data/views.py | 13 +++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/sasdata/fair_database/data/urls.py b/sasdata/fair_database/data/urls.py index 97ba2b8f..cba7e3ed 100644 --- a/sasdata/fair_database/data/urls.py +++ b/sasdata/fair_database/data/urls.py @@ -3,5 +3,11 @@ from . import views urlpatterns = [ - path("list/", views.list_data, name="list public files"), + path("", views.list_data, name = "list public file_ids"), + path("<str:username>/", views.list_data, name = "view users file_ids"), + path("load/<int:db_id>/", views.data_info, name = "views data using file id"), + + path("upload/", views.upload, name = "upload data into db"), + path("upload/<data_id>/", views.upload, name = "update file in data"), + path("<int:data_id>/download/", views.download, name = "download data from db"), ] \ No newline at end of file diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 08219af8..86da092b 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -3,5 +3,14 @@ # Create your views here from django.http import HttpResponse -def list_data(request): - return HttpResponse("Hello World! This is going to display data later.") \ No newline at end of file +def list_data(request, username = None): + return HttpResponse("Hello World! This is going to display data later.") + +def data_info(request, db_id): + return HttpResponse("This is going to allow viewing data file %s." % db_id) + +def upload(request, db_id = None): + return HttpResponse("This is going to allow data uploads.") + +def download(request, data_id): + return HttpResponse("This is going to allow downloads of data %s." % data_id) \ No newline at end of file From 2b4d1a0e3ce91dbb70194a740b7429fcfb3d919d Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 23 Jan 2025 15:16:53 -0500 Subject: [PATCH 006/129] Add requirements.txt --- sasdata/fair_database/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 sasdata/fair_database/requirements.txt diff --git a/sasdata/fair_database/requirements.txt b/sasdata/fair_database/requirements.txt new file mode 100644 index 00000000..d80bd138 --- /dev/null +++ b/sasdata/fair_database/requirements.txt @@ -0,0 +1,2 @@ +django +djangorestframework \ No newline at end of file From eb5b3ca562038d58488ab6ea15e3212a92a15488 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 23 Jan 2025 15:46:32 -0500 Subject: [PATCH 007/129] View for listing data --- sasdata/fair_database/data/views.py | 24 +++++++++++++++++-- .../fair_database/fair_database/settings.py | 1 + 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 86da092b..533b5654 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -1,10 +1,30 @@ from django.shortcuts import render # Create your views here -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseBadRequest +from rest_framework.decorators import api_view +from rest_framework.response import Response +from .models import Data + +@api_view(['GET']) def list_data(request, username = None): - return HttpResponse("Hello World! This is going to display data later.") + if request.method == 'GET': + if username: + data_list = {"user_data_ids": {}} + if username == request.user.username and request.user.is_authenticated: + private_data = Data.objects.filter(current_user=request.user.id) + for x in private_data: + data_list["user_data_ids"][x.id] = x.file_name + else: + return HttpResponseBadRequest("user is not logged in, or username is not same as current user") + else: + public_data = Data.objects.filter(is_public=True) + data_list = {"public_data_ids": {}} + for x in public_data: + data_list["public_data_ids"][x.id] = x.file_name + return Response(data_list) + return HttpResponseBadRequest("not get method") def data_info(request, db_id): return HttpResponse("This is going to allow viewing data file %s." % db_id) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 42b46625..f262b262 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -38,6 +38,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', ] MIDDLEWARE = [ From 7cbccb58c18387dc655d4a691eafc134afb0d476 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 23 Jan 2025 16:16:45 -0500 Subject: [PATCH 008/129] View for specific data --- sasdata/fair_database/data/views.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 533b5654..325a57a2 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -1,10 +1,12 @@ from django.shortcuts import render +from django.shortcuts import get_object_or_404 # Create your views here from django.http import HttpResponse, HttpResponseBadRequest from rest_framework.decorators import api_view from rest_framework.response import Response +from sasdata.dataloader.loader import Loader from .models import Data @api_view(['GET']) @@ -26,8 +28,24 @@ def list_data(request, username = None): return Response(data_list) return HttpResponseBadRequest("not get method") +@api_view(['GET']) def data_info(request, db_id): - return HttpResponse("This is going to allow viewing data file %s." % db_id) + if request.method == 'GET': + loader = Loader() + data_db = get_object_or_404(Data, id=db_id) + if data_db.is_public: + data_list = loader.load(data_db.file.path) + contents = [str(data) for data in data_list] + return_data = {data_db.file_name: contents} + # rewrite with "user.is_authenticated" + elif (data_db.current_user == request.user) and request.user.is_authenticated: + data_list = loader.load(data_db.file.path) + contents = [str(data) for data in data_list] + return_data = {data_db.file_name: contents} + else: + return HttpResponseBadRequest("Database is either not public or wrong auth token") + return Response(return_data) + return HttpResponseBadRequest() def upload(request, db_id = None): return HttpResponse("This is going to allow data uploads.") From ec71bff17a3d07df5091d58425ee7f2e46cc1f4d Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 24 Jan 2025 11:28:52 -0500 Subject: [PATCH 009/129] Remove db.sqlite3 --- sasdata/fair_database/db.sqlite3 | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 sasdata/fair_database/db.sqlite3 diff --git a/sasdata/fair_database/db.sqlite3 b/sasdata/fair_database/db.sqlite3 deleted file mode 100644 index e69de29b..00000000 From 58b57f93e9669879c55be07a11c14d12a7bc2c3b Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 24 Jan 2025 15:18:19 -0500 Subject: [PATCH 010/129] Enable file upload --- sasdata/fair_database/data/forms.py | 8 ++++ sasdata/fair_database/data/serializers.py | 8 ++++ sasdata/fair_database/data/urls.py | 4 +- sasdata/fair_database/data/views.py | 43 +++++++++++++++++-- .../fair_database/fair_database/settings.py | 11 ++++- 5 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 sasdata/fair_database/data/forms.py create mode 100644 sasdata/fair_database/data/serializers.py diff --git a/sasdata/fair_database/data/forms.py b/sasdata/fair_database/data/forms.py new file mode 100644 index 00000000..e336efab --- /dev/null +++ b/sasdata/fair_database/data/forms.py @@ -0,0 +1,8 @@ +from django import forms +from .models import Data + +# Create the form class. +class DataForm(forms.ModelForm): + class Meta: + model = Data + fields = ["file", "is_public"] \ No newline at end of file diff --git a/sasdata/fair_database/data/serializers.py b/sasdata/fair_database/data/serializers.py new file mode 100644 index 00000000..c90249c3 --- /dev/null +++ b/sasdata/fair_database/data/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers + +from .models import Data + +class DataSerializer(serializers.ModelSerializer): + class Meta: + model = Data + fields = "__all__" \ No newline at end of file diff --git a/sasdata/fair_database/data/urls.py b/sasdata/fair_database/data/urls.py index cba7e3ed..abe4ffdb 100644 --- a/sasdata/fair_database/data/urls.py +++ b/sasdata/fair_database/data/urls.py @@ -3,8 +3,8 @@ from . import views urlpatterns = [ - path("", views.list_data, name = "list public file_ids"), - path("<str:username>/", views.list_data, name = "view users file_ids"), + path("list/", views.list_data, name = "list public file_ids"), + path("list/<str:username>/", views.list_data, name = "view users file_ids"), path("load/<int:db_id>/", views.data_info, name = "views data using file id"), path("upload/", views.upload, name = "upload data into db"), diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 325a57a2..898041e1 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -1,13 +1,17 @@ +import os + from django.shortcuts import render from django.shortcuts import get_object_or_404 # Create your views here -from django.http import HttpResponse, HttpResponseBadRequest +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from rest_framework.decorators import api_view from rest_framework.response import Response from sasdata.dataloader.loader import Loader +from .serializers import DataSerializer from .models import Data +from .forms import DataForm @api_view(['GET']) def list_data(request, username = None): @@ -47,8 +51,41 @@ def data_info(request, db_id): return Response(return_data) return HttpResponseBadRequest() -def upload(request, db_id = None): - return HttpResponse("This is going to allow data uploads.") +@api_view(['POST', 'PUT']) +def upload(request, data_id = None, version = None): + #saves file + if request.method == 'POST': + form = DataForm(request.data, request.FILES) + if form.is_valid(): + form.save() + db = Data.objects.get(pk = form.instance.pk) + + if request.user.is_authenticated: + serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path), "current_user" : request.user.id}) + else: + serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path)}) + + + #saves or updates file + elif request.method == 'PUT': + #require data_id + if data_id != None and request.user: + if request.user.is_authenticated: + db = get_object_or_404(Data, current_user = request.user.id, id = data_id) + form = DataForm(request.data, request.FILES, instance=db) + if form.is_valid(): + form.save() + serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path)}, partial = True) + else: + return HttpResponseForbidden("user is not logged in") + else: + return HttpResponseBadRequest() + + if serializer.is_valid(): + serializer.save() + #TODO get warnings/errors later + return_data = {"current_user":request.user.username, "authenticated" : request.user.is_authenticated, "file_id" : db.id, "file_alternative_name":serializer.data["file_name"],"is_public" : serializer.data["is_public"]} + return Response(return_data) def download(request, data_id): return HttpResponse("This is going to allow downloads of data %s." % data_id) \ No newline at end of file diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index f262b262..61ee9dba 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ +import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -115,9 +116,15 @@ # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.1/howto/static-files/ +# https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = 'static/' + +STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATIC_URL = '/static/' + +#instead of doing this, create a create a new media_root +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = '/media/' # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field From aee461786b7920932e02eb89b2e6df9f582e7b8b Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 24 Jan 2025 15:30:38 -0500 Subject: [PATCH 011/129] File upload test script --- .../fair_database/upload_example_data.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 sasdata/fair_database/fair_database/upload_example_data.py diff --git a/sasdata/fair_database/fair_database/upload_example_data.py b/sasdata/fair_database/fair_database/upload_example_data.py new file mode 100644 index 00000000..bc9164c0 --- /dev/null +++ b/sasdata/fair_database/fair_database/upload_example_data.py @@ -0,0 +1,41 @@ +import os +import logging +import requests + +from glob import glob + +EXAMPLE_DATA_DIR = os.environ.get("EXAMPLE_DATA_DIR", '../../example_data') + +def parse_1D(): + dir_1d = os.path.join(EXAMPLE_DATA_DIR, "1d_data") + if not os.path.isdir(dir_1d): + logging.error("1D Data directory not found at: {}".format(dir_1d)) + return + for file_path in glob(os.path.join(dir_1d, "*")): + upload_file(file_path) + +def parse_2D(): + dir_2d = os.path.join(EXAMPLE_DATA_DIR, "2d_data") + if not os.path.isdir(dir_2d): + logging.error("2D Data directory not found at: {}".format(dir_2d)) + return + for file_path in glob(os.path.join(dir_2d, "*")): + upload_file(file_path) + +def parse_sesans(): + sesans_dir = os.path.join(EXAMPLE_DATA_DIR, "sesans_data") + if not os.path.isdir(sesans_dir): + logging.error("Sesans Data directory not found at: {}".format(sesans_dir)) + return + for file_path in glob(os.path.join(sesans_dir, "*")): + upload_file(file_path) + +def upload_file(file_path): + url = 'http://localhost:8000/data/upload/' + file = open(file_path, 'rb') + requests.request('POST', url, data={'is_public': True}, files={'file':file}) + +if __name__ == '__main__': + parse_1D() + parse_2D() + parse_sesans() From 842caf1007abf825143feca0fc2152842ec8f387 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 27 Jan 2025 10:43:17 -0500 Subject: [PATCH 012/129] Add version to url --- sasdata/fair_database/data/views.py | 6 +++--- sasdata/fair_database/fair_database/upload_example_data.py | 2 +- sasdata/fair_database/fair_database/urls.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 898041e1..56206c2f 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -14,7 +14,7 @@ from .forms import DataForm @api_view(['GET']) -def list_data(request, username = None): +def list_data(request, username = None, version = None): if request.method == 'GET': if username: data_list = {"user_data_ids": {}} @@ -33,7 +33,7 @@ def list_data(request, username = None): return HttpResponseBadRequest("not get method") @api_view(['GET']) -def data_info(request, db_id): +def data_info(request, db_id, version = None): if request.method == 'GET': loader = Loader() data_db = get_object_or_404(Data, id=db_id) @@ -87,5 +87,5 @@ def upload(request, data_id = None, version = None): return_data = {"current_user":request.user.username, "authenticated" : request.user.is_authenticated, "file_id" : db.id, "file_alternative_name":serializer.data["file_name"],"is_public" : serializer.data["is_public"]} return Response(return_data) -def download(request, data_id): +def download(request, data_id, version = None): return HttpResponse("This is going to allow downloads of data %s." % data_id) \ No newline at end of file diff --git a/sasdata/fair_database/fair_database/upload_example_data.py b/sasdata/fair_database/fair_database/upload_example_data.py index bc9164c0..bf014cf2 100644 --- a/sasdata/fair_database/fair_database/upload_example_data.py +++ b/sasdata/fair_database/fair_database/upload_example_data.py @@ -31,7 +31,7 @@ def parse_sesans(): upload_file(file_path) def upload_file(file_path): - url = 'http://localhost:8000/data/upload/' + url = 'http://localhost:8000/v1/data/upload/' file = open(file_path, 'rb') requests.request('POST', url, data={'is_public': True}, files={'file':file}) diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py index e223d9d8..e1f9149a 100644 --- a/sasdata/fair_database/fair_database/urls.py +++ b/sasdata/fair_database/fair_database/urls.py @@ -15,9 +15,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import include, path +from django.urls import include, path, re_path urlpatterns = [ - path('data/', include("data.urls")), - path('admin/', admin.site.urls), + re_path(r"^(?P<version>(v1))/data/", include("data.urls")), + path("admin/", admin.site.urls), ] From 553ae9b9d769759644db7753960acb5bc571387c Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 27 Jan 2025 11:22:05 -0500 Subject: [PATCH 013/129] Add file download functionality --- sasdata/fair_database/data/views.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 56206c2f..b7da2ed6 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -2,9 +2,7 @@ from django.shortcuts import render from django.shortcuts import get_object_or_404 - -# Create your views here -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404, FileResponse from rest_framework.decorators import api_view from rest_framework.response import Response @@ -87,5 +85,20 @@ def upload(request, data_id = None, version = None): return_data = {"current_user":request.user.username, "authenticated" : request.user.is_authenticated, "file_id" : db.id, "file_alternative_name":serializer.data["file_name"],"is_public" : serializer.data["is_public"]} return Response(return_data) +#downloads a file def download(request, data_id, version = None): - return HttpResponse("This is going to allow downloads of data %s." % data_id) \ No newline at end of file + if request.method == 'GET': + data = get_object_or_404(Data, id=data_id) + if not data.is_public: + # add session key later + if not request.user.is_authenticated: + return HttpResponseBadRequest("data is private, must log in") + # TODO add issues later + try: + file = open(data.file.path, 'rb') + except Exception as e: + return HttpResponseBadRequest(str(e)) + if file is None: + raise Http404("File not found.") + return FileResponse(file, as_attachment=True) + return HttpResponseBadRequest() \ No newline at end of file From e70ac5b87e2b9eb4f1e33ff9f728515d252a4bec Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 27 Jan 2025 11:47:46 -0500 Subject: [PATCH 014/129] Allow admin page to view data --- sasdata/fair_database/data/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/admin.py b/sasdata/fair_database/data/admin.py index 8c38f3f3..bfe8c7d9 100644 --- a/sasdata/fair_database/data/admin.py +++ b/sasdata/fair_database/data/admin.py @@ -1,3 +1,4 @@ from django.contrib import admin +from .models import Data -# Register your models here. +admin.site.register(Data) \ No newline at end of file From 0d3ceb8ea0e27b768e9c15fe8ca39759f6232902 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 27 Jan 2025 14:11:54 -0500 Subject: [PATCH 015/129] Tests for data list from webfit --- sasdata/fair_database/data/tests.py | 28 +++++++++++++++++++++++++++- sasdata/fair_database/data/views.py | 1 - 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 7ce503c2..d1f3ef13 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -1,3 +1,29 @@ +import os + from django.test import TestCase +from django.contrib.auth.models import User +from rest_framework.test import APIClient + +from .models import Data + +def find(filename): + return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) + +class TestLists(TestCase): + def setUp(self): + public_test_data = Data.objects.create(id = 1, file_name = "cyl_400_40.txt", is_public = True) + public_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) + self.user = User.objects.create_user(username="testUser", password="secret", id = 2) + private_test_data = Data.objects.create(id = 3, current_user = self.user, file_name = "cyl_400_20.txt", is_public = False) + private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + #working + def test_does_list_public(self): + request = self.client.get('/v1/data/list/') + self.assertEqual(request.data, {"public_data_ids":{1:"cyl_400_40.txt"}}) -# Create your tests here. + def test_does_list_user(self): + request = self.client.get('/v1/data/list/testUser/', user = self.user) + self.assertEqual(request.data, {"user_data_ids":{3:"cyl_400_20.txt"}}) \ No newline at end of file diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index b7da2ed6..72f1fa3d 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -1,6 +1,5 @@ import os -from django.shortcuts import render from django.shortcuts import get_object_or_404 from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404, FileResponse from rest_framework.decorators import api_view From c26ec533febdacda0851022d8d23d124a924ab1a Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 27 Jan 2025 14:29:02 -0500 Subject: [PATCH 016/129] Tests for data upload from webfit --- sasdata/fair_database/data/tests.py | 72 ++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index d1f3ef13..e416ac84 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -1,8 +1,11 @@ import os +import shutil +from django.conf import settings from django.test import TestCase from django.contrib.auth.models import User -from rest_framework.test import APIClient +from rest_framework.test import APIClient, APITestCase +from rest_framework import status from .models import Data @@ -26,4 +29,69 @@ def test_does_list_public(self): def test_does_list_user(self): request = self.client.get('/v1/data/list/testUser/', user = self.user) - self.assertEqual(request.data, {"user_data_ids":{3:"cyl_400_20.txt"}}) \ No newline at end of file + self.assertEqual(request.data, {"user_data_ids":{3:"cyl_400_20.txt"}}) + + def test_does_load_data_info_public(self): + request = self.client.get('/v1/data/load/1/') + print(request.data) + self.assertEqual(request.status_code, status.HTTP_200_OK) + + def test_does_load_data_info_private(self): + request = self.client.get('/v1/data/load/3/') + print(request.data) + self.assertEqual(request.status_code, status.HTTP_200_OK) + + def tearDown(self): + shutil.rmtree(settings.MEDIA_ROOT) + +class TestingDatabase(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username="testUser", password="secret", id = 1) + self.data = Data.objects.create(id = 2, current_user = self.user, file_name = "cyl_400_20.txt", is_public = False) + self.data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + self.client2 = APIClient() + + def test_is_data_being_created(self): + file = open(find("cyl_400_40.txt"), 'rb') + data = { + "is_public":False, + "file":file + } + request = self.client.post('/v1/data/upload/', data=data) + self.assertEqual(request.status_code, status.HTTP_200_OK) + self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) + Data.objects.get(id = 3).delete() + + def test_is_data_being_created_no_user(self): + file = open(find("cyl_400_40.txt"), 'rb') + data = { + "is_public":False, + "file":file + } + request = self.client2.post('/v1/data/upload/', data=data) + self.assertEqual(request.status_code, status.HTTP_200_OK) + self.assertEqual(request.data, {"current_user":'', "authenticated" : False, "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) + Data.objects.get(id = 3).delete() + + def test_does_file_upload_update(self): + file = open(find("cyl_400_40.txt")) + data = { + "file":file, + "is_public":False + } + request = self.client.put('/v1/data/upload/2/', data = data) + request2 = self.client2.put('/v1/data/upload/2/', data = data) + self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, "file_id" : 2, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) + self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) + Data.objects.get(id = 2).delete() + + #TODO write tests for download + ''' + def test_does_download(self): + self.client.get() + ''' + + def tearDown(self): + shutil.rmtree(settings.MEDIA_ROOT) \ No newline at end of file From a38fb5b736462279efd1d7a268eee49266a9962d Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 27 Jan 2025 14:58:16 -0500 Subject: [PATCH 017/129] Disallow downloading unowned private data --- sasdata/fair_database/data/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 72f1fa3d..6cba5191 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -92,6 +92,8 @@ def download(request, data_id, version = None): # add session key later if not request.user.is_authenticated: return HttpResponseBadRequest("data is private, must log in") + if not request.user == data.current_user: + return HttpResponseBadRequest("data is private") # TODO add issues later try: file = open(data.file.path, 'rb') From 724423fd4031fa36a231edb8cd42beef798579d3 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Feb 2025 12:57:54 -0500 Subject: [PATCH 018/129] Create app for authentication-related stuff --- sasdata/fair_database/user_app/__init__.py | 0 sasdata/fair_database/user_app/admin.py | 3 +++ sasdata/fair_database/user_app/apps.py | 6 ++++++ sasdata/fair_database/user_app/migrations/__init__.py | 0 sasdata/fair_database/user_app/models.py | 3 +++ sasdata/fair_database/user_app/tests.py | 3 +++ sasdata/fair_database/user_app/views.py | 3 +++ 7 files changed, 18 insertions(+) create mode 100644 sasdata/fair_database/user_app/__init__.py create mode 100644 sasdata/fair_database/user_app/admin.py create mode 100644 sasdata/fair_database/user_app/apps.py create mode 100644 sasdata/fair_database/user_app/migrations/__init__.py create mode 100644 sasdata/fair_database/user_app/models.py create mode 100644 sasdata/fair_database/user_app/tests.py create mode 100644 sasdata/fair_database/user_app/views.py diff --git a/sasdata/fair_database/user_app/__init__.py b/sasdata/fair_database/user_app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sasdata/fair_database/user_app/admin.py b/sasdata/fair_database/user_app/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/sasdata/fair_database/user_app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/sasdata/fair_database/user_app/apps.py b/sasdata/fair_database/user_app/apps.py new file mode 100644 index 00000000..f2d1d417 --- /dev/null +++ b/sasdata/fair_database/user_app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'user_app' diff --git a/sasdata/fair_database/user_app/migrations/__init__.py b/sasdata/fair_database/user_app/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sasdata/fair_database/user_app/models.py b/sasdata/fair_database/user_app/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/sasdata/fair_database/user_app/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/sasdata/fair_database/user_app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/sasdata/fair_database/user_app/views.py b/sasdata/fair_database/user_app/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/sasdata/fair_database/user_app/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From ad7520f423b98537c3dee2f2d9b803ab55d132f4 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Feb 2025 14:53:12 -0500 Subject: [PATCH 019/129] Install allauth --- sasdata/fair_database/fair_database/settings.py | 9 +++++++++ sasdata/fair_database/fair_database/urls.py | 1 + 2 files changed, 10 insertions(+) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 61ee9dba..26eafcc4 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -40,6 +40,10 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.orcid', ] MIDDLEWARE = [ @@ -50,6 +54,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'allauth.account.middleware.AccountMiddleware', ] ROOT_URLCONF = 'fair_database.urls' @@ -72,6 +77,10 @@ WSGI_APPLICATION = 'fair_database.wsgi.application' +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +) # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py index e1f9149a..dae6d9a7 100644 --- a/sasdata/fair_database/fair_database/urls.py +++ b/sasdata/fair_database/fair_database/urls.py @@ -20,4 +20,5 @@ urlpatterns = [ re_path(r"^(?P<version>(v1))/data/", include("data.urls")), path("admin/", admin.site.urls), + path("accounts/", include("allauth.urls")), ] From d328cfd0f7d54ced7f3b01215ffac1a0b8475218 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Feb 2025 16:13:40 -0500 Subject: [PATCH 020/129] Headless allauth --- sasdata/fair_database/fair_database/settings.py | 1 + sasdata/fair_database/fair_database/urls.py | 1 + 2 files changed, 2 insertions(+) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 26eafcc4..aafc984f 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -42,6 +42,7 @@ 'rest_framework', 'allauth', 'allauth.account', + 'allauth.headless', 'allauth.socialaccount', 'allauth.socialaccount.providers.orcid', ] diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py index dae6d9a7..43acdfc5 100644 --- a/sasdata/fair_database/fair_database/urls.py +++ b/sasdata/fair_database/fair_database/urls.py @@ -21,4 +21,5 @@ re_path(r"^(?P<version>(v1))/data/", include("data.urls")), path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), + path("_allauth/", include("allauth.headless.urls")), ] From 9de4157fcd6e5842890d07e849c43f84ff929c6d Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Feb 2025 16:14:14 -0500 Subject: [PATCH 021/129] Test for download --- sasdata/fair_database/data/tests.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index e416ac84..b9140f60 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -88,10 +88,15 @@ def test_does_file_upload_update(self): Data.objects.get(id = 2).delete() #TODO write tests for download - ''' + def test_does_download(self): - self.client.get() - ''' + request = self.client.get('/v1/data/2/download/') + print('Starting download tests') + self.assertEqual(request.status_code, status.HTTP_200_OK) + file_contents = b''.join(request.streaming_content) + test_file = open(find('cyl_400_20.txt'), 'rb') + self.assertEqual(file_contents, test_file.read()) + def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) \ No newline at end of file From 41b33de2d86b67e0e05f0dca7d37dfd8b0f33cba Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Feb 2025 16:24:58 -0500 Subject: [PATCH 022/129] Change unauthorized download response to 403 forbidden --- sasdata/fair_database/data/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 6cba5191..958724ab 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -85,15 +85,16 @@ def upload(request, data_id = None, version = None): return Response(return_data) #downloads a file +@api_view(['GET']) def download(request, data_id, version = None): if request.method == 'GET': data = get_object_or_404(Data, id=data_id) if not data.is_public: # add session key later if not request.user.is_authenticated: - return HttpResponseBadRequest("data is private, must log in") + return HttpResponseForbidden("data is private, must log in") if not request.user == data.current_user: - return HttpResponseBadRequest("data is private") + return HttpResponseForbidden("data is private") # TODO add issues later try: file = open(data.file.path, 'rb') From c4bfdf342b1a3555bf7f0f7bcab29832687cec8a Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Feb 2025 16:25:41 -0500 Subject: [PATCH 023/129] Add unauthorized download test --- sasdata/fair_database/data/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index b9140f60..7add1db9 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -91,8 +91,9 @@ def test_does_file_upload_update(self): def test_does_download(self): request = self.client.get('/v1/data/2/download/') - print('Starting download tests') + request2 = self.client2.get('/v1/data/2/download/') self.assertEqual(request.status_code, status.HTTP_200_OK) + self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) file_contents = b''.join(request.streaming_content) test_file = open(find('cyl_400_20.txt'), 'rb') self.assertEqual(file_contents, test_file.read()) From 0fd97b9b7d2bfb7378a1adfe2ebcb4f24c5794d2 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Feb 2025 16:36:47 -0500 Subject: [PATCH 024/129] Start authentication tests --- sasdata/fair_database/user_app/tests.py | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index 7ce503c2..b761dede 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -1,3 +1,36 @@ +import requests + from django.test import TestCase +from rest_framework import status +from rest_framework.test import RequestsClient # Create your tests here. +class AuthTests(TestCase): + + def setup(self): + self.client = RequestsClient() + + def test_register(self): + data = { + 'email': 'test@test.com', + 'username': 'testUser', + 'password': 'testPassword' + } + response = self.client.post('/_allauth/app/v1/auth/signup',data=data) + print(response.content) + self.assertEqual(response.status_code, status.HTTP_200_OK) + +#can register a user, user is w/in User model +# user is logged in after registration +# logged-in user can create Data, is data's current_user +# test log out + + +# Permissions +# Any user can access public data +# logged-in user can access and modify their own private data +# unauthenticated user cannot access private data +# unauthenticated user cannot modify data +# logged-in user cannot modify data other than their own +# logged-in user cannot access the private data of others + From 49a478de28b3c81173a4937dffde721bcf81d064 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 4 Feb 2025 14:32:55 -0500 Subject: [PATCH 025/129] Install dj-rest-auth --- sasdata/fair_database/fair_database/settings.py | 11 ++++++++++- sasdata/fair_database/fair_database/urls.py | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index aafc984f..1d3f9455 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -39,14 +39,20 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sites', 'rest_framework', + 'rest_framework.authtoken', 'allauth', 'allauth.account', - 'allauth.headless', + #'allauth.headless', 'allauth.socialaccount', 'allauth.socialaccount.providers.orcid', + 'dj_rest_auth', + 'dj_rest_auth.registration', ] +SITE_ID = 1 + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -83,6 +89,9 @@ 'allauth.account.auth_backends.AuthenticationBackend', ) +HEADLESS_ONLY = False +ACCOUNT_EMAIL_VERIFICATION = 'none' + # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py index 43acdfc5..ebb1f4f8 100644 --- a/sasdata/fair_database/fair_database/urls.py +++ b/sasdata/fair_database/fair_database/urls.py @@ -22,4 +22,6 @@ path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), path("_allauth/", include("allauth.headless.urls")), + path('dj-rest-auth/', include('dj_rest_auth.urls')), + path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')), ] From 49b5ac295cd525f286e2ceadab8ed4ca0a8e3fc2 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 4 Feb 2025 15:28:42 -0500 Subject: [PATCH 026/129] Tests for register and login --- sasdata/fair_database/fair_database/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py index ebb1f4f8..ac37b3cb 100644 --- a/sasdata/fair_database/fair_database/urls.py +++ b/sasdata/fair_database/fair_database/urls.py @@ -20,8 +20,8 @@ urlpatterns = [ re_path(r"^(?P<version>(v1))/data/", include("data.urls")), path("admin/", admin.site.urls), - path("accounts/", include("allauth.urls")), - path("_allauth/", include("allauth.headless.urls")), + path("accounts/", include("allauth.urls")), #needed for social auth + #path("_allauth/", include("allauth.headless.urls")), path('dj-rest-auth/', include('dj_rest_auth.urls')), path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')), ] From 24d7ffc5533fc971a7fa037149ed580d0af2316b Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 4 Feb 2025 15:54:43 -0500 Subject: [PATCH 027/129] Tests for logout --- sasdata/fair_database/user_app/tests.py | 55 ++++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index b761dede..c0e19f75 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -2,24 +2,59 @@ from django.test import TestCase from rest_framework import status -from rest_framework.test import RequestsClient +from rest_framework.test import APIClient + +from django.contrib.auth.models import User # Create your tests here. class AuthTests(TestCase): - def setup(self): - self.client = RequestsClient() + def setUp(self): + self.client = APIClient() + self.register_data = { + "email": "email@domain.org", + "username": "testUser", + "password1": "sasview!", + "password2": "sasview!" + } + self.login_data = { + "username": "testUser", + "email": "email@domain.org", + "password": "sasview!" + } def test_register(self): - data = { - 'email': 'test@test.com', - 'username': 'testUser', - 'password': 'testPassword' - } - response = self.client.post('/_allauth/app/v1/auth/signup',data=data) - print(response.content) + response = self.client.post('/dj-rest-auth/registration/',data=self.register_data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + user = User.objects.get(username="testUser") + self.assertEquals(user.email, self.register_data["email"]) + + def test_login(self): + user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + response = self.client.post('/dj-rest-auth/login', data=self.login_data) self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_login_logout(self): + user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + self.client.post('/dj-rest-auth/login', data=self.login_data) + response = self.client.post('/dj-rest-auth/logout') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') + + def test_register_logout(self): + self.client.post('/dj-rest-auth/registration/', data=self.register_data) + response = self.client.post('/dj-rest-auth/logout') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') + + def test_register_login(self): + register_response = self.client.post('/dj-rest-auth/registration/', data=self.register_data) + logout_response = self.client.post('/dj-rest-auth/logout') + login_response = self.client.post('/dj-rest-auth/login', data=self.login_data) + self.assertEqual(register_response.status_code, status.HTTP_201_CREATED) + self.assertEqual(logout_response.status_code, status.HTTP_200_OK) + self.assertEqual(login_response.status_code, status.HTTP_200_OK) + #can register a user, user is w/in User model # user is logged in after registration # logged-in user can create Data, is data's current_user From 300caf8aa65a8ef9ab3a3e48bcaace47231f3b29 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 4 Feb 2025 16:14:21 -0500 Subject: [PATCH 028/129] Test for password change --- sasdata/fair_database/user_app/tests.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index c0e19f75..c9cd1943 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -23,6 +23,9 @@ def setUp(self): "password": "sasview!" } + def tearDown(self): + self.client.post('/dj-rest-auth/logout') + def test_register(self): response = self.client.post('/dj-rest-auth/registration/',data=self.register_data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -55,6 +58,16 @@ def test_register_login(self): self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) + def test_password_change(self): + self.client.post('/dj-rest-auth/registration/', data=self.register_data) + data = { + "new_password1": "sasview?", + "new_password2": "sasview?", + "old_password": "sasview!" + } + response = self.client.post('/dj-rest-auth/password/change', data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + #can register a user, user is w/in User model # user is logged in after registration # logged-in user can create Data, is data's current_user From 717b9da3b153e50d44753629838d3571f571e2ed Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 4 Feb 2025 16:26:55 -0500 Subject: [PATCH 029/129] Test for password change --- sasdata/fair_database/user_app/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index c9cd1943..4fb7827a 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -65,8 +65,14 @@ def test_password_change(self): "new_password2": "sasview?", "old_password": "sasview!" } + l_data = self.login_data + l_data["password"] = "sasview?" response = self.client.post('/dj-rest-auth/password/change', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) + logout_response = self.client.post('/dj-rest-auth/logout') + login_response = self.client.post('/dj-rest-auth/login', data=l_data) + self.assertEqual(logout_response.status_code, status.HTTP_200_OK) + self.assertEqual(login_response.status_code, status.HTTP_200_OK) #can register a user, user is w/in User model # user is logged in after registration From b2b00f01314f559b106d07becaa345955ce85b84 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 11:20:56 -0500 Subject: [PATCH 030/129] Tests for user endpoint --- sasdata/fair_database/user_app/tests.py | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index 4fb7827a..6fd314dc 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -37,6 +37,38 @@ def test_login(self): response = self.client.post('/dj-rest-auth/login', data=self.login_data) self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_user_get(self): + user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + self.client.force_authenticate(user=user) + response = self.client.get('/dj-rest-auth/user') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, + b'{"pk":1,"username":"testUser","email":"email@domain.org","first_name":"","last_name":""}') + + def test_user_put_username(self): + user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + self.client.force_authenticate(user=user) + data = { + "username": "newName" + } + response = self.client.put('/dj-rest-auth/user', data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, + b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}') + + def test_user_put_name(self): + user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + self.client.force_authenticate(user=user) + data = { + "username": "newName", + "first_name": "Clark", + "last_name": "Kent" + } + response = self.client.put('/dj-rest-auth/user', data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, + b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}') + def test_login_logout(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") self.client.post('/dj-rest-auth/login', data=self.login_data) From 1e24e749dd96d54af8bba7c13f9a9ba7b5476550 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 11:28:49 -0500 Subject: [PATCH 031/129] Test user endpoint unauthenticated --- sasdata/fair_database/user_app/tests.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index 6fd314dc..ba0df62c 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -54,7 +54,7 @@ def test_user_put_username(self): response = self.client.put('/dj-rest-auth/user', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, - b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}') + b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}') def test_user_put_name(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") @@ -67,7 +67,14 @@ def test_user_put_name(self): response = self.client.put('/dj-rest-auth/user', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, - b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}') + b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}') + + def test_user_unauthenticated(self): + response = self.client.get('/dj-rest-auth/user') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + print(response.content) + self.assertEqual(response.content, + b'{"detail":"Authentication credentials were not provided."}') def test_login_logout(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") From 34db98c2f6dab7858823e9676c591571d1e44531 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 11:34:58 -0500 Subject: [PATCH 032/129] Add checks to register/login/logout tests --- sasdata/fair_database/user_app/tests.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index ba0df62c..b66c47e7 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -31,11 +31,15 @@ def test_register(self): self.assertEqual(response.status_code, status.HTTP_201_CREATED) user = User.objects.get(username="testUser") self.assertEquals(user.email, self.register_data["email"]) + response2 = self.client.get('/dj-rest-auth/user') + self.assertEquals(response2.status_code, status.HTTP_200_OK) def test_login(self): - user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") response = self.client.post('/dj-rest-auth/login', data=self.login_data) self.assertEqual(response.status_code, status.HTTP_200_OK) + response2 = self.client.get('/dj-rest-auth/user') + self.assertEquals(response2.status_code, status.HTTP_200_OK) def test_user_get(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") @@ -72,22 +76,25 @@ def test_user_put_name(self): def test_user_unauthenticated(self): response = self.client.get('/dj-rest-auth/user') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - print(response.content) self.assertEqual(response.content, b'{"detail":"Authentication credentials were not provided."}') def test_login_logout(self): - user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") self.client.post('/dj-rest-auth/login', data=self.login_data) response = self.client.post('/dj-rest-auth/logout') + response2 = self.client.get('/dj-rest-auth/user') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') + self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) def test_register_logout(self): self.client.post('/dj-rest-auth/registration/', data=self.register_data) response = self.client.post('/dj-rest-auth/logout') + response2 = self.client.get('/dj-rest-auth/user') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') + self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) def test_register_login(self): register_response = self.client.post('/dj-rest-auth/registration/', data=self.register_data) From 50b667eeaa50814aede1aaf7838898e0bd47ab52 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 11:54:50 -0500 Subject: [PATCH 033/129] Reorganize auth url patterns --- sasdata/fair_database/fair_database/urls.py | 4 +- sasdata/fair_database/user_app/tests.py | 44 ++++++++++----------- sasdata/fair_database/user_app/urls.py | 12 ++++++ 3 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 sasdata/fair_database/user_app/urls.py diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py index ac37b3cb..89bac77c 100644 --- a/sasdata/fair_database/fair_database/urls.py +++ b/sasdata/fair_database/fair_database/urls.py @@ -21,7 +21,5 @@ re_path(r"^(?P<version>(v1))/data/", include("data.urls")), path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), #needed for social auth - #path("_allauth/", include("allauth.headless.urls")), - path('dj-rest-auth/', include('dj_rest_auth.urls')), - path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')), + path('auth/', include('user_app.urls')), ] diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index b66c47e7..6262201a 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -24,27 +24,27 @@ def setUp(self): } def tearDown(self): - self.client.post('/dj-rest-auth/logout') + self.client.post('/auth/logout') def test_register(self): - response = self.client.post('/dj-rest-auth/registration/',data=self.register_data) + response = self.client.post('/auth/registration/',data=self.register_data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) user = User.objects.get(username="testUser") self.assertEquals(user.email, self.register_data["email"]) - response2 = self.client.get('/dj-rest-auth/user') + response2 = self.client.get('/auth/user') self.assertEquals(response2.status_code, status.HTTP_200_OK) def test_login(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") - response = self.client.post('/dj-rest-auth/login', data=self.login_data) + response = self.client.post('/auth/login', data=self.login_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response2 = self.client.get('/dj-rest-auth/user') + response2 = self.client.get('/auth/user') self.assertEquals(response2.status_code, status.HTTP_200_OK) def test_user_get(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") self.client.force_authenticate(user=user) - response = self.client.get('/dj-rest-auth/user') + response = self.client.get('/auth/user') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"pk":1,"username":"testUser","email":"email@domain.org","first_name":"","last_name":""}') @@ -55,7 +55,7 @@ def test_user_put_username(self): data = { "username": "newName" } - response = self.client.put('/dj-rest-auth/user', data=data) + response = self.client.put('/auth/user', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}') @@ -68,44 +68,44 @@ def test_user_put_name(self): "first_name": "Clark", "last_name": "Kent" } - response = self.client.put('/dj-rest-auth/user', data=data) + response = self.client.put('/auth/user', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}') def test_user_unauthenticated(self): - response = self.client.get('/dj-rest-auth/user') + response = self.client.get('/auth/user') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.content, b'{"detail":"Authentication credentials were not provided."}') def test_login_logout(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") - self.client.post('/dj-rest-auth/login', data=self.login_data) - response = self.client.post('/dj-rest-auth/logout') - response2 = self.client.get('/dj-rest-auth/user') + self.client.post('/auth/login', data=self.login_data) + response = self.client.post('/auth/logout') + response2 = self.client.get('/auth/user') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) def test_register_logout(self): - self.client.post('/dj-rest-auth/registration/', data=self.register_data) - response = self.client.post('/dj-rest-auth/logout') - response2 = self.client.get('/dj-rest-auth/user') + self.client.post('/auth/registration/', data=self.register_data) + response = self.client.post('/auth/logout') + response2 = self.client.get('/auth/user') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) def test_register_login(self): - register_response = self.client.post('/dj-rest-auth/registration/', data=self.register_data) - logout_response = self.client.post('/dj-rest-auth/logout') - login_response = self.client.post('/dj-rest-auth/login', data=self.login_data) + register_response = self.client.post('/auth/registration/', data=self.register_data) + logout_response = self.client.post('/auth/logout') + login_response = self.client.post('/auth/login', data=self.login_data) self.assertEqual(register_response.status_code, status.HTTP_201_CREATED) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) def test_password_change(self): - self.client.post('/dj-rest-auth/registration/', data=self.register_data) + self.client.post('/auth/registration/', data=self.register_data) data = { "new_password1": "sasview?", "new_password2": "sasview?", @@ -113,10 +113,10 @@ def test_password_change(self): } l_data = self.login_data l_data["password"] = "sasview?" - response = self.client.post('/dj-rest-auth/password/change', data=data) + response = self.client.post('/auth/password/change', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - logout_response = self.client.post('/dj-rest-auth/logout') - login_response = self.client.post('/dj-rest-auth/login', data=l_data) + logout_response = self.client.post('/auth/logout') + login_response = self.client.post('/auth/login', data=l_data) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) diff --git a/sasdata/fair_database/user_app/urls.py b/sasdata/fair_database/user_app/urls.py new file mode 100644 index 00000000..e6188dd3 --- /dev/null +++ b/sasdata/fair_database/user_app/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from dj_rest_auth.views import (LoginView, LogoutView, + UserDetailsView, PasswordChangeView) +from dj_rest_auth.registration.views import RegisterView + +urlpatterns = [ + path('register/', RegisterView.as_view(), name='register'), + path('login/', LoginView.as_view(), name='login'), + path('logout/', LogoutView.as_view(), name='logout'), + path('user/', UserDetailsView.as_view(), name='view user information'), + path('password/change/', PasswordChangeView.as_view(), name='change password'), +] \ No newline at end of file From ae8cfc703667bed36dea8d0561430d9f4df23b83 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 13:50:04 -0500 Subject: [PATCH 034/129] Create views for knox auth --- .../fair_database/fair_database/settings.py | 19 +++++++++- sasdata/fair_database/user_app/serializers.py | 11 ++++++ sasdata/fair_database/user_app/views.py | 37 ++++++++++++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 sasdata/fair_database/user_app/serializers.py diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 1d3f9455..c0ab3a01 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -44,11 +44,11 @@ 'rest_framework.authtoken', 'allauth', 'allauth.account', - #'allauth.headless', 'allauth.socialaccount', 'allauth.socialaccount.providers.orcid', 'dj_rest_auth', 'dj_rest_auth.registration', + 'knox', ] SITE_ID = 1 @@ -89,7 +89,22 @@ 'allauth.account.auth_backends.AuthenticationBackend', ) -HEADLESS_ONLY = False +REST_FRAMEWORK = { + 'DEFAULT ATHENTICATION CLASSES': ('knox.auth.TokenAuthentication'), + 'DEFAULT_FILTER_BACKENDS': ( + 'django_filters.rest_framework.DjangoFilterBackend', + ), +} + +REST_AUTH_TOKEN_MODEL = 'knox.models.AuthToken' +REST_AUTH_TOKEN_CREATOR = 'project.apps.accounts.utils.create_knox_token' + +REST_AUTH_SERIALIZERS = { + 'USER_DETAILS_SERIALIZER': 'project.apps.accounts.serializers.UserDetailsSerializer', + 'TOKEN_SERIALIZER': 'project.apps.accounts.serializers.KnoxSerializer', +} + +HEADLESS_ONLY = True ACCOUNT_EMAIL_VERIFICATION = 'none' # Database diff --git a/sasdata/fair_database/user_app/serializers.py b/sasdata/fair_database/user_app/serializers.py new file mode 100644 index 00000000..c443afd1 --- /dev/null +++ b/sasdata/fair_database/user_app/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from rest_auth.serializers import UserDetailsSerializer + + +class KnoxSerializer(serializers.Serializer): + """ + Serializer for Knox authentication. + """ + token = serializers.CharField() + user = UserDetailsSerializer() \ No newline at end of file diff --git a/sasdata/fair_database/user_app/views.py b/sasdata/fair_database/user_app/views.py index 91ea44a2..4474ef28 100644 --- a/sasdata/fair_database/user_app/views.py +++ b/sasdata/fair_database/user_app/views.py @@ -1,3 +1,36 @@ -from django.shortcuts import render +from rest_framework.response import Response -# Create your views here. +from dj_rest_auth.views import LoginView +from dj_rest_auth.registration.views import RegisterView +from knox.models import AuthToken + +from allauth.account.utils import complete_signup +from allauth.account import app_settings as allauth_settings + +from serializers import KnoxSerializer + + +class KnoxLoginView(LoginView): + + def get_response(self): + serializer_class = self.get_response_serializer() + + data = { + 'user': self.user, + 'token': self.token + } + serializer = serializer_class(instance=data, context={'request': self.request}) + + return Response(serializer.data, status=200) + +#do we want to use email? +class KnoxRegisterView(RegisterView): + + def get_response_data(self, user): + return KnoxSerializer({'user': user, 'token': self.token}).data + + def perform_create(self, serializer): + user = serializer.save(self.request) + self.token = AuthToken.objects.create(user=user) + complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, None) + return user \ No newline at end of file From b58363963c0ee3f48b93b373c8a642bb82fae6aa Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 13:52:28 -0500 Subject: [PATCH 035/129] Add authentication app to installed apps --- sasdata/fair_database/fair_database/settings.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index c0ab3a01..6b03229c 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -49,6 +49,7 @@ 'dj_rest_auth', 'dj_rest_auth.registration', 'knox', + 'user_app.apps.UserAppConfig', ] SITE_ID = 1 @@ -91,9 +92,6 @@ REST_FRAMEWORK = { 'DEFAULT ATHENTICATION CLASSES': ('knox.auth.TokenAuthentication'), - 'DEFAULT_FILTER_BACKENDS': ( - 'django_filters.rest_framework.DjangoFilterBackend', - ), } REST_AUTH_TOKEN_MODEL = 'knox.models.AuthToken' From 10261652af05785e3db3fcb4395b0582e68e3921 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 16:32:46 -0500 Subject: [PATCH 036/129] Switch auth to use knox --- sasdata/fair_database/fair_database/settings.py | 11 +++++------ sasdata/fair_database/user_app/serializers.py | 7 +++++-- sasdata/fair_database/user_app/urls.py | 8 ++++---- sasdata/fair_database/user_app/util.py | 6 ++++++ sasdata/fair_database/user_app/views.py | 13 +++++++++---- 5 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 sasdata/fair_database/user_app/util.py diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 6b03229c..633a0c9b 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -94,12 +94,11 @@ 'DEFAULT ATHENTICATION CLASSES': ('knox.auth.TokenAuthentication'), } -REST_AUTH_TOKEN_MODEL = 'knox.models.AuthToken' -REST_AUTH_TOKEN_CREATOR = 'project.apps.accounts.utils.create_knox_token' - -REST_AUTH_SERIALIZERS = { - 'USER_DETAILS_SERIALIZER': 'project.apps.accounts.serializers.UserDetailsSerializer', - 'TOKEN_SERIALIZER': 'project.apps.accounts.serializers.KnoxSerializer', +REST_AUTH = { + 'TOKEN_SERIALIZER': 'user_app.serializers.KnoxSerializer', + 'USER_DETAILS_SERIALIZER': 'dj_rest_auth.serializers.UserDetailsSerializer', + 'TOKEN_MODEL': 'knox.models.AuthToken', + 'TOKEN_CREATOR': 'user_app.util.create_knox_token', } HEADLESS_ONLY = True diff --git a/sasdata/fair_database/user_app/serializers.py b/sasdata/fair_database/user_app/serializers.py index c443afd1..5181a735 100644 --- a/sasdata/fair_database/user_app/serializers.py +++ b/sasdata/fair_database/user_app/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from rest_auth.serializers import UserDetailsSerializer +from dj_rest_auth.serializers import UserDetailsSerializer class KnoxSerializer(serializers.Serializer): @@ -8,4 +8,7 @@ class KnoxSerializer(serializers.Serializer): Serializer for Knox authentication. """ token = serializers.CharField() - user = UserDetailsSerializer() \ No newline at end of file + user = UserDetailsSerializer() + + def get_token(self, obj): + return obj["token"][1] \ No newline at end of file diff --git a/sasdata/fair_database/user_app/urls.py b/sasdata/fair_database/user_app/urls.py index e6188dd3..791a778d 100644 --- a/sasdata/fair_database/user_app/urls.py +++ b/sasdata/fair_database/user_app/urls.py @@ -1,11 +1,11 @@ from django.urls import path -from dj_rest_auth.views import (LoginView, LogoutView, +from dj_rest_auth.views import (LogoutView, UserDetailsView, PasswordChangeView) -from dj_rest_auth.registration.views import RegisterView +from .views import KnoxLoginView, KnoxRegisterView urlpatterns = [ - path('register/', RegisterView.as_view(), name='register'), - path('login/', LoginView.as_view(), name='login'), + path('register/', KnoxRegisterView.as_view(), name='register'), + path('login/', KnoxLoginView.as_view(), name='login'), path('logout/', LogoutView.as_view(), name='logout'), path('user/', UserDetailsView.as_view(), name='view user information'), path('password/change/', PasswordChangeView.as_view(), name='change password'), diff --git a/sasdata/fair_database/user_app/util.py b/sasdata/fair_database/user_app/util.py new file mode 100644 index 00000000..ab9bcd0d --- /dev/null +++ b/sasdata/fair_database/user_app/util.py @@ -0,0 +1,6 @@ +from knox.models import AuthToken + + +def create_knox_token(token_model, user, serializer): + token = AuthToken.objects.create(user=user) + return token \ No newline at end of file diff --git a/sasdata/fair_database/user_app/views.py b/sasdata/fair_database/user_app/views.py index 4474ef28..b32e0c26 100644 --- a/sasdata/fair_database/user_app/views.py +++ b/sasdata/fair_database/user_app/views.py @@ -1,17 +1,22 @@ -from rest_framework.response import Response +from django.conf import settings +from rest_framework.response import Response from dj_rest_auth.views import LoginView from dj_rest_auth.registration.views import RegisterView from knox.models import AuthToken - from allauth.account.utils import complete_signup from allauth.account import app_settings as allauth_settings -from serializers import KnoxSerializer +from .serializers import KnoxSerializer +from .util import create_knox_token class KnoxLoginView(LoginView): + '''def get_response_serializer(self): + response_serializer = settings.REST_AUTH_SERIALIZERS['TOKEN_SERIALIZER'] + return response_serializer''' + def get_response(self): serializer_class = self.get_response_serializer() @@ -31,6 +36,6 @@ def get_response_data(self, user): def perform_create(self, serializer): user = serializer.save(self.request) - self.token = AuthToken.objects.create(user=user) + self.token = create_knox_token(None,user,None) complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, None) return user \ No newline at end of file From 28ddb55ace91b2fa5501885041f029e58752a989 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 16:33:39 -0500 Subject: [PATCH 037/129] Fix tests to match url changes --- sasdata/fair_database/user_app/tests.py | 45 +++++++++++++------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index 6262201a..7cfb80f5 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -23,28 +23,29 @@ def setUp(self): "password": "sasview!" } + ''' def tearDown(self): - self.client.post('/auth/logout') + self.client.post('/auth/logout/') ''' def test_register(self): - response = self.client.post('/auth/registration/',data=self.register_data) + response = self.client.post('/auth/register/',data=self.register_data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) user = User.objects.get(username="testUser") self.assertEquals(user.email, self.register_data["email"]) - response2 = self.client.get('/auth/user') + response2 = self.client.get('/auth/user/') self.assertEquals(response2.status_code, status.HTTP_200_OK) def test_login(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") - response = self.client.post('/auth/login', data=self.login_data) + response = self.client.post('/auth/login/', data=self.login_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response2 = self.client.get('/auth/user') + response2 = self.client.get('/auth/user/') self.assertEquals(response2.status_code, status.HTTP_200_OK) def test_user_get(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") self.client.force_authenticate(user=user) - response = self.client.get('/auth/user') + response = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"pk":1,"username":"testUser","email":"email@domain.org","first_name":"","last_name":""}') @@ -55,7 +56,7 @@ def test_user_put_username(self): data = { "username": "newName" } - response = self.client.put('/auth/user', data=data) + response = self.client.put('/auth/user/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}') @@ -68,44 +69,44 @@ def test_user_put_name(self): "first_name": "Clark", "last_name": "Kent" } - response = self.client.put('/auth/user', data=data) + response = self.client.put('/auth/user/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}') def test_user_unauthenticated(self): - response = self.client.get('/auth/user') + response = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.content, b'{"detail":"Authentication credentials were not provided."}') def test_login_logout(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") - self.client.post('/auth/login', data=self.login_data) - response = self.client.post('/auth/logout') - response2 = self.client.get('/auth/user') + self.client.post('/auth/login/', data=self.login_data) + response = self.client.post('/auth/logout/') + response2 = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) def test_register_logout(self): - self.client.post('/auth/registration/', data=self.register_data) - response = self.client.post('/auth/logout') - response2 = self.client.get('/auth/user') + self.client.post('/auth/register/', data=self.register_data) + response = self.client.post('/auth/logout/') + response2 = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) def test_register_login(self): - register_response = self.client.post('/auth/registration/', data=self.register_data) - logout_response = self.client.post('/auth/logout') - login_response = self.client.post('/auth/login', data=self.login_data) + register_response = self.client.post('/auth/register/', data=self.register_data) + logout_response = self.client.post('/auth/logout/') + login_response = self.client.post('/auth/login/', data=self.login_data) self.assertEqual(register_response.status_code, status.HTTP_201_CREATED) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) def test_password_change(self): - self.client.post('/auth/registration/', data=self.register_data) + self.client.post('/auth/register/', data=self.register_data) data = { "new_password1": "sasview?", "new_password2": "sasview?", @@ -113,10 +114,10 @@ def test_password_change(self): } l_data = self.login_data l_data["password"] = "sasview?" - response = self.client.post('/auth/password/change', data=data) + response = self.client.post('/auth/password/change/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - logout_response = self.client.post('/auth/logout') - login_response = self.client.post('/auth/login', data=l_data) + logout_response = self.client.post('/auth/logout/') + login_response = self.client.post('/auth/login/', data=l_data) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) From 39eecd35dc876ac66ac4aef383e5efbfcdb1f08b Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Feb 2025 16:52:27 -0500 Subject: [PATCH 038/129] Add view for orcid login --- sasdata/fair_database/user_app/views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sasdata/fair_database/user_app/views.py b/sasdata/fair_database/user_app/views.py index b32e0c26..8772ac5c 100644 --- a/sasdata/fair_database/user_app/views.py +++ b/sasdata/fair_database/user_app/views.py @@ -2,10 +2,10 @@ from rest_framework.response import Response from dj_rest_auth.views import LoginView -from dj_rest_auth.registration.views import RegisterView -from knox.models import AuthToken +from dj_rest_auth.registration.views import RegisterView, SocialLoginView from allauth.account.utils import complete_signup from allauth.account import app_settings as allauth_settings +from allauth.socialaccount.providers.orcid.view import OrcidOAuth2Adapter from .serializers import KnoxSerializer from .util import create_knox_token @@ -38,4 +38,7 @@ def perform_create(self, serializer): user = serializer.save(self.request) self.token = create_knox_token(None,user,None) complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, None) - return user \ No newline at end of file + return user + +class OrcidLoginView(SocialLoginView): + adapter_class = OrcidOAuth2Adapter \ No newline at end of file From 576ff0aa94b76adb2590fe65fa66404a03ab0aec Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 10:20:07 -0500 Subject: [PATCH 039/129] Set up future ORCID support --- .../fair_database/fair_database/settings.py | 25 +++++++++++++++++++ sasdata/fair_database/user_app/urls.py | 3 ++- sasdata/fair_database/user_app/views.py | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 633a0c9b..c298e1bd 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -85,6 +85,7 @@ WSGI_APPLICATION = 'fair_database.wsgi.application' +#Authentication AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', @@ -104,6 +105,30 @@ HEADLESS_ONLY = True ACCOUNT_EMAIL_VERIFICATION = 'none' +#to enable ORCID, register for credentials through ORCID and fill out client_id and secret +SOCIALACCOUNT_PROVIDERS = { + 'orcid': { + 'APPS': [ + { + 'client_id': '', + 'secret': '', + 'key': '', + } + + ], + 'SCOPE': [ + 'profile', 'email', + ], + 'AUTH_PARAMETERS': { + 'access_type': 'online' + }, + # Base domain of the API. Default value: 'orcid.org', for the production API + 'BASE_DOMAIN':'sandbox.orcid.org', # for the sandbox API + # Member API or Public API? Default: False (for the public API) + 'MEMBER_API': False, + } +} + # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases diff --git a/sasdata/fair_database/user_app/urls.py b/sasdata/fair_database/user_app/urls.py index 791a778d..07805182 100644 --- a/sasdata/fair_database/user_app/urls.py +++ b/sasdata/fair_database/user_app/urls.py @@ -1,7 +1,7 @@ from django.urls import path from dj_rest_auth.views import (LogoutView, UserDetailsView, PasswordChangeView) -from .views import KnoxLoginView, KnoxRegisterView +from .views import KnoxLoginView, KnoxRegisterView, OrcidLoginView urlpatterns = [ path('register/', KnoxRegisterView.as_view(), name='register'), @@ -9,4 +9,5 @@ path('logout/', LogoutView.as_view(), name='logout'), path('user/', UserDetailsView.as_view(), name='view user information'), path('password/change/', PasswordChangeView.as_view(), name='change password'), + path('login/orcid/', OrcidLoginView.as_view(), name='orcid login') ] \ No newline at end of file diff --git a/sasdata/fair_database/user_app/views.py b/sasdata/fair_database/user_app/views.py index 8772ac5c..d8a4d20f 100644 --- a/sasdata/fair_database/user_app/views.py +++ b/sasdata/fair_database/user_app/views.py @@ -5,7 +5,7 @@ from dj_rest_auth.registration.views import RegisterView, SocialLoginView from allauth.account.utils import complete_signup from allauth.account import app_settings as allauth_settings -from allauth.socialaccount.providers.orcid.view import OrcidOAuth2Adapter +from allauth.socialaccount.providers.orcid.views import OrcidOAuth2Adapter from .serializers import KnoxSerializer from .util import create_knox_token From 6a60f9ca039e6c9efc4e3c23b0ada65833c75419 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 10:35:38 -0500 Subject: [PATCH 040/129] Documentation of authentication stuff --- sasdata/fair_database/fair_database/settings.py | 5 +++-- sasdata/fair_database/user_app/urls.py | 2 ++ sasdata/fair_database/user_app/views.py | 8 +++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index c298e1bd..c96d7b93 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -85,7 +85,7 @@ WSGI_APPLICATION = 'fair_database.wsgi.application' -#Authentication +# Authentication AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', @@ -102,10 +102,11 @@ 'TOKEN_CREATOR': 'user_app.util.create_knox_token', } +# allauth settings HEADLESS_ONLY = True ACCOUNT_EMAIL_VERIFICATION = 'none' -#to enable ORCID, register for credentials through ORCID and fill out client_id and secret +# to enable ORCID, register for credentials through ORCID and fill out client_id and secret SOCIALACCOUNT_PROVIDERS = { 'orcid': { 'APPS': [ diff --git a/sasdata/fair_database/user_app/urls.py b/sasdata/fair_database/user_app/urls.py index 07805182..339d7d8b 100644 --- a/sasdata/fair_database/user_app/urls.py +++ b/sasdata/fair_database/user_app/urls.py @@ -3,6 +3,8 @@ UserDetailsView, PasswordChangeView) from .views import KnoxLoginView, KnoxRegisterView, OrcidLoginView +'''Urls for authentication. Orcid login not functional.''' + urlpatterns = [ path('register/', KnoxRegisterView.as_view(), name='register'), path('login/', KnoxLoginView.as_view(), name='login'), diff --git a/sasdata/fair_database/user_app/views.py b/sasdata/fair_database/user_app/views.py index d8a4d20f..88eb2eec 100644 --- a/sasdata/fair_database/user_app/views.py +++ b/sasdata/fair_database/user_app/views.py @@ -10,13 +10,10 @@ from .serializers import KnoxSerializer from .util import create_knox_token +#Login using knox tokens rather than django-rest-framework tokens. class KnoxLoginView(LoginView): - '''def get_response_serializer(self): - response_serializer = settings.REST_AUTH_SERIALIZERS['TOKEN_SERIALIZER'] - return response_serializer''' - def get_response(self): serializer_class = self.get_response_serializer() @@ -28,7 +25,7 @@ def get_response(self): return Response(serializer.data, status=200) -#do we want to use email? +# Registration using knox tokens rather than django-rest-framework tokens. class KnoxRegisterView(RegisterView): def get_response_data(self, user): @@ -40,5 +37,6 @@ def perform_create(self, serializer): complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, None) return user +# For ORCID login class OrcidLoginView(SocialLoginView): adapter_class = OrcidOAuth2Adapter \ No newline at end of file From 6f39355d7e6ea7f87b5d49500e566e5ec2adaa57 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 11:55:40 -0500 Subject: [PATCH 041/129] Authentication test documentation --- sasdata/fair_database/user_app/tests.py | 30 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index 7cfb80f5..cd816380 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -27,21 +27,24 @@ def setUp(self): def tearDown(self): self.client.post('/auth/logout/') ''' + # Test if registration successfully creates a new user and logs in def test_register(self): response = self.client.post('/auth/register/',data=self.register_data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) user = User.objects.get(username="testUser") - self.assertEquals(user.email, self.register_data["email"]) response2 = self.client.get('/auth/user/') - self.assertEquals(response2.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(user.email, self.register_data["email"]) + self.assertEqual(response2.status_code, status.HTTP_200_OK) + # Test if login successful def test_login(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") response = self.client.post('/auth/login/', data=self.login_data) - self.assertEqual(response.status_code, status.HTTP_200_OK) response2 = self.client.get('/auth/user/') - self.assertEquals(response2.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response2.status_code, status.HTTP_200_OK) + # Test get user information def test_user_get(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") self.client.force_authenticate(user=user) @@ -50,6 +53,7 @@ def test_user_get(self): self.assertEqual(response.content, b'{"pk":1,"username":"testUser","email":"email@domain.org","first_name":"","last_name":""}') + # Test changing username def test_user_put_username(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") self.client.force_authenticate(user=user) @@ -61,6 +65,7 @@ def test_user_put_username(self): self.assertEqual(response.content, b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}') + # Test changing username and first and last name def test_user_put_name(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") self.client.force_authenticate(user=user) @@ -74,12 +79,14 @@ def test_user_put_name(self): self.assertEqual(response.content, b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}') + # Test user info inaccessible when unauthenticated def test_user_unauthenticated(self): response = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.content, b'{"detail":"Authentication credentials were not provided."}') + # Test logout is successful after login def test_login_logout(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") self.client.post('/auth/login/', data=self.login_data) @@ -87,16 +94,18 @@ def test_login_logout(self): response2 = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') - self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) + # Test logout is successful after registration def test_register_logout(self): self.client.post('/auth/register/', data=self.register_data) response = self.client.post('/auth/logout/') response2 = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') - self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) + # Test login is successful after registering then logging out def test_register_login(self): register_response = self.client.post('/auth/register/', data=self.register_data) logout_response = self.client.post('/auth/logout/') @@ -105,6 +114,7 @@ def test_register_login(self): self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) + # Test password is successfully changed def test_password_change(self): self.client.post('/auth/register/', data=self.register_data) data = { @@ -115,16 +125,14 @@ def test_password_change(self): l_data = self.login_data l_data["password"] = "sasview?" response = self.client.post('/auth/password/change/', data=data) - self.assertEqual(response.status_code, status.HTTP_200_OK) logout_response = self.client.post('/auth/logout/') login_response = self.client.post('/auth/login/', data=l_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) -#can register a user, user is w/in User model -# user is logged in after registration + # logged-in user can create Data, is data's current_user -# test log out # Permissions From b314fe951d60ea756554dd6a305330218dc6f3e6 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 12:08:22 -0500 Subject: [PATCH 042/129] Documentation for data tests --- sasdata/fair_database/data/tests.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 7add1db9..a415308e 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -22,20 +22,23 @@ def setUp(self): self.client = APIClient() self.client.force_authenticate(user=self.user) - #working + # Test list public data def test_does_list_public(self): request = self.client.get('/v1/data/list/') self.assertEqual(request.data, {"public_data_ids":{1:"cyl_400_40.txt"}}) + # Test list a user's private data def test_does_list_user(self): request = self.client.get('/v1/data/list/testUser/', user = self.user) self.assertEqual(request.data, {"user_data_ids":{3:"cyl_400_20.txt"}}) + # Test loading a public data file def test_does_load_data_info_public(self): request = self.client.get('/v1/data/load/1/') print(request.data) self.assertEqual(request.status_code, status.HTTP_200_OK) + # Test loading private data with authorization def test_does_load_data_info_private(self): request = self.client.get('/v1/data/load/3/') print(request.data) @@ -53,6 +56,7 @@ def setUp(self): self.client.force_authenticate(user=self.user) self.client2 = APIClient() + # Test data upload creates data in database def test_is_data_being_created(self): file = open(find("cyl_400_40.txt"), 'rb') data = { @@ -64,6 +68,7 @@ def test_is_data_being_created(self): self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) Data.objects.get(id = 3).delete() + # Test data upload w/out authenticated user def test_is_data_being_created_no_user(self): file = open(find("cyl_400_40.txt"), 'rb') data = { @@ -75,6 +80,7 @@ def test_is_data_being_created_no_user(self): self.assertEqual(request.data, {"current_user":'', "authenticated" : False, "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) Data.objects.get(id = 3).delete() + # Test updating file def test_does_file_upload_update(self): file = open(find("cyl_400_40.txt")) data = { @@ -87,8 +93,7 @@ def test_does_file_upload_update(self): self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) Data.objects.get(id = 2).delete() - #TODO write tests for download - + # Test file download def test_does_download(self): request = self.client.get('/v1/data/2/download/') request2 = self.client2.get('/v1/data/2/download/') From ab760f911238d98af22f6debc852df7a2f09503d Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 13:45:33 -0500 Subject: [PATCH 043/129] Add auth packages to requirements.txt --- sasdata/fair_database/requirements.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/requirements.txt b/sasdata/fair_database/requirements.txt index d80bd138..5eb1b6b9 100644 --- a/sasdata/fair_database/requirements.txt +++ b/sasdata/fair_database/requirements.txt @@ -1,2 +1,7 @@ +#this requirements extends the base sasview requirements files +#to get both you will need to run this after base requirements files django -djangorestframework \ No newline at end of file +djangorestframework +dj-rest-auth +django-allauth +django-rest-knox From b25542331239d96ba3465f6b042c4476771788a7 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 13:53:48 -0500 Subject: [PATCH 044/129] Test login/logout multiple clients same account --- sasdata/fair_database/user_app/tests.py | 26 +++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index cd816380..f019c29a 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -23,10 +23,6 @@ def setUp(self): "password": "sasview!" } - ''' - def tearDown(self): - self.client.post('/auth/logout/') ''' - # Test if registration successfully creates a new user and logs in def test_register(self): response = self.client.post('/auth/register/',data=self.register_data) @@ -44,6 +40,16 @@ def test_login(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) + # Test simultaneous login by multiple clients + def test_multiple_login(self): + User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + client2 = APIClient() + response = self.client.post('/auth/login/', data=self.login_data) + response2 = client2.post('/auth/login/', data=self.login_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response2.status_code, status.HTTP_200_OK) + self.assertNotEqual(response.content, response2.content) + # Test get user information def test_user_get(self): user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") @@ -105,6 +111,18 @@ def test_register_logout(self): self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) + def test_multiple_logout(self): + User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + client2 = APIClient() + self.client.post('/auth/login/', data=self.login_data) + client2.post('/auth/login/', data=self.login_data) + response = self.client.post('/auth/logout/') + response2 = client2.get('/auth/user/') + response3 = client2.post('/auth/logout/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response2.status_code, status.HTTP_200_OK) + self.assertEqual(response3.status_code, status.HTTP_200_OK) + # Test login is successful after registering then logging out def test_register_login(self): register_response = self.client.post('/auth/register/', data=self.register_data) From 4aad5791c7f79de7cafd34b1e1deca0ce793a5e8 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 14:09:07 -0500 Subject: [PATCH 045/129] Break up data tests --- sasdata/fair_database/data/tests.py | 41 ++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index a415308e..67a24ce3 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -14,10 +14,12 @@ def find(filename): class TestLists(TestCase): def setUp(self): - public_test_data = Data.objects.create(id = 1, file_name = "cyl_400_40.txt", is_public = True) + public_test_data = Data.objects.create(id = 1, file_name = "cyl_400_40.txt", + is_public = True) public_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) self.user = User.objects.create_user(username="testUser", password="secret", id = 2) - private_test_data = Data.objects.create(id = 3, current_user = self.user, file_name = "cyl_400_20.txt", is_public = False) + private_test_data = Data.objects.create(id = 3, current_user = self.user, + file_name = "cyl_400_20.txt", is_public = False) private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) self.client = APIClient() self.client.force_authenticate(user=self.user) @@ -35,13 +37,11 @@ def test_does_list_user(self): # Test loading a public data file def test_does_load_data_info_public(self): request = self.client.get('/v1/data/load/1/') - print(request.data) self.assertEqual(request.status_code, status.HTTP_200_OK) # Test loading private data with authorization def test_does_load_data_info_private(self): request = self.client.get('/v1/data/load/3/') - print(request.data) self.assertEqual(request.status_code, status.HTTP_200_OK) def tearDown(self): @@ -50,7 +50,8 @@ def tearDown(self): class TestingDatabase(APITestCase): def setUp(self): self.user = User.objects.create_user(username="testUser", password="secret", id = 1) - self.data = Data.objects.create(id = 2, current_user = self.user, file_name = "cyl_400_20.txt", is_public = False) + self.data = Data.objects.create(id = 2, current_user = self.user, + file_name = "cyl_400_20.txt", is_public = False) self.data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) self.client = APIClient() self.client.force_authenticate(user=self.user) @@ -65,7 +66,8 @@ def test_is_data_being_created(self): } request = self.client.post('/v1/data/upload/', data=data) self.assertEqual(request.status_code, status.HTTP_200_OK) - self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) + self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, + "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) Data.objects.get(id = 3).delete() # Test data upload w/out authenticated user @@ -77,7 +79,8 @@ def test_is_data_being_created_no_user(self): } request = self.client2.post('/v1/data/upload/', data=data) self.assertEqual(request.status_code, status.HTTP_200_OK) - self.assertEqual(request.data, {"current_user":'', "authenticated" : False, "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) + self.assertEqual(request.data, {"current_user":'', "authenticated" : False, + "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) Data.objects.get(id = 3).delete() # Test updating file @@ -88,21 +91,33 @@ def test_does_file_upload_update(self): "is_public":False } request = self.client.put('/v1/data/upload/2/', data = data) - request2 = self.client2.put('/v1/data/upload/2/', data = data) - self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, "file_id" : 2, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) - self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, + "file_id" : 2, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) Data.objects.get(id = 2).delete() + # Test file upload update fails when unauthorized + def test_unauthorized_file_upload_update(self): + file = open(find("cyl_400_40.txt")) + data = { + "file": file, + "is_public": False + } + request = self.client2.put('/v1/data/upload/2/', data=data) + self.assertEqual(request.status_code, status.HTTP_403_FORBIDDEN) + Data.objects.get(id=2).delete() + # Test file download def test_does_download(self): request = self.client.get('/v1/data/2/download/') - request2 = self.client2.get('/v1/data/2/download/') - self.assertEqual(request.status_code, status.HTTP_200_OK) - self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) file_contents = b''.join(request.streaming_content) test_file = open(find('cyl_400_20.txt'), 'rb') + self.assertEqual(request.status_code, status.HTTP_200_OK) self.assertEqual(file_contents, test_file.read()) + # Test file download fails when unauthorized + def test_unauthorized_download(self): + request2 = self.client2.get('/v1/data/2/download/') + self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) \ No newline at end of file From 10583bef103f39e2418aa45012e79e6290420fc4 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 14:43:06 -0500 Subject: [PATCH 046/129] Create class for auth/data permissions tests --- .../fair_database/tests/test_permissions.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 sasdata/fair_database/fair_database/tests/test_permissions.py diff --git a/sasdata/fair_database/fair_database/tests/test_permissions.py b/sasdata/fair_database/fair_database/tests/test_permissions.py new file mode 100644 index 00000000..26f01e4d --- /dev/null +++ b/sasdata/fair_database/fair_database/tests/test_permissions.py @@ -0,0 +1,60 @@ +import os + +from django.contrib.auth.models import User +from rest_framework.test import APIClient, APITestCase + +from data.models import Data + +def find(filename): + return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) + +class DataListPermissionsTests(APITestCase): + ''' Test permissions of data views using user_app for authentication. ''' + + def setUp(self): + self.user = User.objects.create_user(username="testUser", password="secret", id=1) + self.user2 = User.objects.create_user(username="testUser2", password="secret", id=2) + public_test_data = Data.objects.create(id=1, file_name="cyl_400_40.txt", + is_public=True) + public_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) + private_test_data = Data.objects.create(id=2, current_user=self.user, + file_name="cyl_400_20.txt", is_public=False) + private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) + + # Authenticated user can view list of data + + # Unauthenticated user can view list of public data + + # Authenticated user cannot view other users' private data on list + + # Authenticated user can load public data + + # Authenticated user can load own private data + + # Authenticated user cannot load others' private data + + # Unauthenticated user can load public data + + # Unauthenticated user cannot load others' private data + + # Authenticated user can upload data + + # ***Unauthenticated user can upload public data + + # Unauthenticated user cannot upload private data + + # Authenticated user can update own public data + + # Authenticated user can update own private data + + # Authenticated user cannot update unowned public data + + # Unauthenticated user cannot update data + + # Anyone can download public data + + # Authenticated user can download own data + + # Authenticated user cannot download others' data + + # Unauthenticated user cannot download private data From 144cb2311f5ce76936708fd10e5266af4cda0403 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 15:14:11 -0500 Subject: [PATCH 047/129] Tests for list data permissions --- sasdata/fair_database/__init__.py | 0 .../fair_database/test_permissions.py | 106 ++++++++++++++++++ .../fair_database/tests/test_permissions.py | 60 ---------- 3 files changed, 106 insertions(+), 60 deletions(-) create mode 100644 sasdata/fair_database/__init__.py create mode 100644 sasdata/fair_database/fair_database/test_permissions.py delete mode 100644 sasdata/fair_database/fair_database/tests/test_permissions.py diff --git a/sasdata/fair_database/__init__.py b/sasdata/fair_database/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py new file mode 100644 index 00000000..247af357 --- /dev/null +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -0,0 +1,106 @@ +import os + +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + +from data.models import Data + +def find(filename): + return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) + +class DataListPermissionsTests(APITestCase): + ''' Test permissions of data views using user_app for authentication. ''' + + def setUp(self): + self.user = User.objects.create_user(username="testUser", password="secret", id=1, + email="email@domain.com") + self.user2 = User.objects.create_user(username="testUser2", password="secret", id=2, + email="email2@domain.com") + unowned_test_data = Data.objects.create(id=1, file_name="cyl_400_40.txt", + is_public=True) + unowned_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) + private_test_data = Data.objects.create(id=2, current_user=self.user, + file_name="cyl_400_20.txt", is_public=False) + private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) + public_test_data = Data.objects.create(id=3, current_user=self.user, + file_name="cyl_testdata.txt", is_public=True) + public_test_data.file.save("cyl_testdata.txt", open(find("cyl_testdata.txt"), 'rb')) + self.login_data_1 = { + 'username': 'testUser', + 'password': 'secret', + 'email': 'email@domain.com' + } + self.login_data_2 = { + 'username': 'testUser2', + 'password': 'secret', + 'email': 'email2@domain.com' + } + + # Authenticated user can view list of data + # TODO: change to reflect inclusion of owned private data + def test_list_authenticated(self): + self.client.post('/auth/login/', data=self.login_data_1) + response = self.client.get('/v1/data/list/') + response2 = self.client.get('/v1/data/list/testUser/') + self.assertEqual(response.data, + {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) + self.assertEqual(response2.data, + {"user_data_ids": {2: "cyl_400_20.txt", 3: "cyl_testdata.txt"}}) + + # Authenticated user cannot view other users' private data on list + # TODO: Change response codes + def test_list_authenticated_2(self): + self.client.post('/auth/login/', data=self.login_data_2) + response = self.client.get('/v1/data/list/') + response2 = self.client.get('/v1/data/list/testUser/') + response3 = self.client.get('/v1/data/list/testUser2/') + self.assertEqual(response.data, + {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) + self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response3.data, {"user_data_ids": {}}) + + # Unauthenticated user can view list of public data + def test_list_unauthenticated(self): + response = self.client.get('/v1/data/list/') + response2 = self.client.get('/v1/data/list/testUser/') + self.assertEqual(response.data, + {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) + self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) + + + # Authenticated user can load public data + def test_load_authenticated_public(self): + self.client.post('/auth/login/', data=self.login_data_1) + response = self.client.get('/v1/data/load/1/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Authenticated user can load own private data + + # Authenticated user cannot load others' private data + + # Unauthenticated user can load public data + + # Unauthenticated user cannot load others' private data + + # Authenticated user can upload data + + # ***Unauthenticated user can upload public data + + # Unauthenticated user cannot upload private data + + # Authenticated user can update own public data + + # Authenticated user can update own private data + + # Authenticated user cannot update unowned public data + + # Unauthenticated user cannot update data + + # Anyone can download public data + + # Authenticated user can download own data + + # Authenticated user cannot download others' data + + # Unauthenticated user cannot download private data diff --git a/sasdata/fair_database/fair_database/tests/test_permissions.py b/sasdata/fair_database/fair_database/tests/test_permissions.py deleted file mode 100644 index 26f01e4d..00000000 --- a/sasdata/fair_database/fair_database/tests/test_permissions.py +++ /dev/null @@ -1,60 +0,0 @@ -import os - -from django.contrib.auth.models import User -from rest_framework.test import APIClient, APITestCase - -from data.models import Data - -def find(filename): - return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) - -class DataListPermissionsTests(APITestCase): - ''' Test permissions of data views using user_app for authentication. ''' - - def setUp(self): - self.user = User.objects.create_user(username="testUser", password="secret", id=1) - self.user2 = User.objects.create_user(username="testUser2", password="secret", id=2) - public_test_data = Data.objects.create(id=1, file_name="cyl_400_40.txt", - is_public=True) - public_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) - private_test_data = Data.objects.create(id=2, current_user=self.user, - file_name="cyl_400_20.txt", is_public=False) - private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) - - # Authenticated user can view list of data - - # Unauthenticated user can view list of public data - - # Authenticated user cannot view other users' private data on list - - # Authenticated user can load public data - - # Authenticated user can load own private data - - # Authenticated user cannot load others' private data - - # Unauthenticated user can load public data - - # Unauthenticated user cannot load others' private data - - # Authenticated user can upload data - - # ***Unauthenticated user can upload public data - - # Unauthenticated user cannot upload private data - - # Authenticated user can update own public data - - # Authenticated user can update own private data - - # Authenticated user cannot update unowned public data - - # Unauthenticated user cannot update data - - # Anyone can download public data - - # Authenticated user can download own data - - # Authenticated user cannot download others' data - - # Unauthenticated user cannot download private data From 90f0a233e1ca1ebd026a366fdec4d95f69390a10 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 15:30:05 -0500 Subject: [PATCH 048/129] Tests for data load permissions --- .../fair_database/test_permissions.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index 247af357..a8f13cc2 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -68,20 +68,30 @@ def test_list_unauthenticated(self): {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) - - # Authenticated user can load public data - def test_load_authenticated_public(self): + # Authenticated user can load public data and owned private data + def test_load_authenticated(self): self.client.post('/auth/login/', data=self.login_data_1) response = self.client.get('/v1/data/load/1/') + response2 = self.client.get('/v1/data/load/2/') self.assertEqual(response.status_code, status.HTTP_200_OK) - - # Authenticated user can load own private data + self.assertEqual(response2.status_code, status.HTTP_200_OK) # Authenticated user cannot load others' private data + def test_load_unauthorized(self): + self.client.post('/auth/login/', data=self.login_data_2) + response = self.client.get('/v1/data/load/2/') + response2 = self.client.get('/v1/data/load/3/') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response2.status_code, status.HTTP_200_OK) - # Unauthenticated user can load public data - - # Unauthenticated user cannot load others' private data + # Unauthenticated user can load public data only + def test_load_unauthenticated(self): + response = self.client.get('/v1/data/load/1/') + response2 = self.client.get('/v1/data/load/2/') + response3 = self.client.get('/v1/data/load/3/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response3.status_code, status.HTTP_200_OK) # Authenticated user can upload data From 71d11f9d0354bac19806d33e556c875d30fdc55a Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 15:50:25 -0500 Subject: [PATCH 049/129] Tests for data update --- .../fair_database/test_permissions.py | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index a8f13cc2..5942e7e9 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -1,5 +1,7 @@ import os +import shutil +from django.conf import settings from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APIClient, APITestCase @@ -99,13 +101,50 @@ def test_load_unauthenticated(self): # Unauthenticated user cannot upload private data - # Authenticated user can update own public data - - # Authenticated user can update own private data + # Authenticated user can update own data + def test_upload_put_authenticated(self): + self.client.post('/auth/login/', data=self.login_data_1) + data = { + "is_public": False + } + response = self.client.put('/v1/data/upload/2/', data=data) + response2 = self.client.put('/v1/data/upload/3/', data=data) + self.assertEqual(response.data, + {"current_user": 'testUser', "authenticated": True, "file_id": 2, + "file_alternative_name": "cyl_400_20.txt", "is_public": False}) + self.assertEqual(response2.data, + {"current_user": 'testUser', "authenticated": True, "file_id": 3, + "file_alternative_name": "cyl_testdata.txt", "is_public": False}) + Data.objects.get(id=3).is_public = True - # Authenticated user cannot update unowned public data + # Authenticated user cannot update unowned data + def test_upload_put_unauthorized(self): + self.client.post('/auth/login/', data=self.login_data_2) + file = open(find("cyl_400_40.txt")) + data = { + "file": file, + "is_public": False + } + response = self.client.put('/v1/data/upload/1/', data=data) + response2 = self.client.put('/v1/data/upload/2/', data=data) + response3 = self.client.put('/v1/data/upload/3/', data=data) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response2.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response3.status_code, status.HTTP_404_NOT_FOUND) # Unauthenticated user cannot update data + def test_upload_put_unauthenticated(self): + file = open(find("cyl_400_40.txt")) + data = { + "file": file, + "is_public": False + } + response = self.client.put('/v1/data/upload/1/', data=data) + response2 = self.client.put('/v1/data/upload/2/', data=data) + response3 = self.client.put('/v1/data/upload/3/', data=data) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response3.status_code, status.HTTP_403_FORBIDDEN) # Anyone can download public data @@ -114,3 +153,6 @@ def test_load_unauthenticated(self): # Authenticated user cannot download others' data # Unauthenticated user cannot download private data + + def tearDown(self): + shutil.rmtree(settings.MEDIA_ROOT) \ No newline at end of file From b1f183a33f7a80125095bedf6a0ad09c29301e17 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Feb 2025 16:02:33 -0500 Subject: [PATCH 050/129] Tests for download permissions --- .../fair_database/test_permissions.py | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index 5942e7e9..8fbba1cb 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -146,13 +146,32 @@ def test_upload_put_unauthenticated(self): self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response3.status_code, status.HTTP_403_FORBIDDEN) - # Anyone can download public data - - # Authenticated user can download own data + # Authenticated user can download public and own data + def test_download_authenticated(self): + self.client.post('/auth/login/', data=self.login_data_1) + response = self.client.get('/v1/data/1/download/') + response2 = self.client.get('/v1/data/2/download/') + response3 = self.client.get('/v1/data/3/download/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response2.status_code, status.HTTP_200_OK) + self.assertEqual(response3.status_code, status.HTTP_200_OK) # Authenticated user cannot download others' data + def test_download_unauthorized(self): + self.client.post('/auth/login/', data=self.login_data_2) + response = self.client.get('/v1/data/2/download/') + response2 = self.client.get('/v1/data/3/download/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response2.status_code, status.HTTP_200_OK) # Unauthenticated user cannot download private data + def test_download_unauthenticated(self): + response = self.client.get('/v1/data/1/download/') + response2 = self.client.get('/v1/data/2/download/') + response3 = self.client.get('/v1/data/3/download/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response3.status_code, status.HTTP_200_OK) def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) \ No newline at end of file From fd33147a727b21cd20533c24292a517c123d6570 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 7 Feb 2025 15:55:30 -0500 Subject: [PATCH 051/129] Tests for uploading files --- .../fair_database/test_permissions.py | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index 8fbba1cb..ff62e5e0 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -96,10 +96,38 @@ def test_load_unauthenticated(self): self.assertEqual(response3.status_code, status.HTTP_200_OK) # Authenticated user can upload data - - # ***Unauthenticated user can upload public data - - # Unauthenticated user cannot upload private data + def test_upload_authenticated(self): + self.client.post('/auth/login/', data=self.login_data_1) + file = open(find('cyl_testdata1.txt'), 'rb') + data = { + 'file': file, + 'is_public': False + } + response = self.client.post('/v1/data/upload/', data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {"current_user": 'testUser', "authenticated": True, + "file_id": 4, "file_alternative_name": "cyl_testdata1.txt", "is_public": False}) + Data.objects.get(id=4).delete() + + # Unauthenticated user can upload public data only + def test_upload_unauthenticated(self): + file = open(find('cyl_testdata2.txt'), 'rb') + file2 = open(find('cyl_testdata2.txt'), 'rb') + data = { + 'file': file, + 'is_public': True + } + data2 = { + 'file': file2, + 'is_public': False + } + response = self.client.post('/v1/data/upload/', data=data) + response2 = self.client.post('/v1/data/upload/', data=data2) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {"current_user": '', "authenticated": False, + "file_id": 4, "file_alternative_name": "cyl_testdata2.txt", + "is_public": True}) + self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) # Authenticated user can update own data def test_upload_put_authenticated(self): From 992e38f82925f0d026032d4057f846cb870c5608 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 7 Feb 2025 16:24:25 -0500 Subject: [PATCH 052/129] Test for updating one's own public data --- sasdata/fair_database/data/tests.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 67a24ce3..9800dde5 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -7,7 +7,7 @@ from rest_framework.test import APIClient, APITestCase from rest_framework import status -from .models import Data +from data.models import Data def find(filename): return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) @@ -95,6 +95,20 @@ def test_does_file_upload_update(self): "file_id" : 2, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) Data.objects.get(id = 2).delete() + def test_public_file_upload_update(self): + data_object = Data.objects.create(id=3, current_user=self.user, + file_name="cyl_testdata.txt", is_public=True) + data_object.file.save("cyl_testdata.txt", open(find("cyl_testdata.txt"), 'rb')) + file = open(find("cyl_testdata1.txt")) + data = { + "file": file, + "is_public": True + } + request = self.client.put('/v1/data/upload/3/', data=data) + self.assertEqual(request.data, {"current_user": 'testUser', "authenticated": True, + "file_id": 3, "file_alternative_name": "cyl_testdata1.txt", "is_public": True}) + Data.objects.get(id=3).delete() + # Test file upload update fails when unauthorized def test_unauthorized_file_upload_update(self): file = open(find("cyl_400_40.txt")) From fd8d3fe98df55b28cda7dcfa4eb04816c8fc99e5 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 7 Feb 2025 16:26:14 -0500 Subject: [PATCH 053/129] Allow saving a new file with put --- sasdata/fair_database/data/views.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 958724ab..96d6e4e6 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -50,8 +50,8 @@ def data_info(request, db_id, version = None): @api_view(['POST', 'PUT']) def upload(request, data_id = None, version = None): - #saves file - if request.method == 'POST': + # saves file + if request.method in ['POST', 'PUT'] and data_id == None: form = DataForm(request.data, request.FILES) if form.is_valid(): form.save() @@ -62,21 +62,16 @@ def upload(request, data_id = None, version = None): else: serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path)}) - - #saves or updates file + # updates file elif request.method == 'PUT': - #require data_id - if data_id != None and request.user: - if request.user.is_authenticated: - db = get_object_or_404(Data, current_user = request.user.id, id = data_id) - form = DataForm(request.data, request.FILES, instance=db) - if form.is_valid(): - form.save() - serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path)}, partial = True) - else: - return HttpResponseForbidden("user is not logged in") + if request.user.is_authenticated: + db = get_object_or_404(Data, current_user = request.user.id, id = data_id) + form = DataForm(request.data, request.FILES, instance=db) + if form.is_valid(): + form.save() + serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path)}, partial = True) else: - return HttpResponseBadRequest() + return HttpResponseForbidden("user is not logged in") if serializer.is_valid(): serializer.save() From bd37872d83d7a5990e6d3c52c8d8945b363df9fc Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 7 Feb 2025 16:40:24 -0500 Subject: [PATCH 054/129] Require private data have an owner --- sasdata/fair_database/data/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/serializers.py b/sasdata/fair_database/data/serializers.py index c90249c3..99030aa5 100644 --- a/sasdata/fair_database/data/serializers.py +++ b/sasdata/fair_database/data/serializers.py @@ -5,4 +5,10 @@ class DataSerializer(serializers.ModelSerializer): class Meta: model = Data - fields = "__all__" \ No newline at end of file + fields = "__all__" + + def validate(self, data): + print(data) + if not data['is_public'] and not data['current_user']: + raise serializers.ValidationError('private data must have an owner') + return data \ No newline at end of file From af5b869c7ddec10951aa645e57e25e0bf39e92f4 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 10 Feb 2025 14:23:35 -0500 Subject: [PATCH 055/129] Disallow uploading private unowned data --- sasdata/fair_database/data/serializers.py | 5 +++-- sasdata/fair_database/data/tests.py | 4 ++-- sasdata/fair_database/data/views.py | 12 ++++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/sasdata/fair_database/data/serializers.py b/sasdata/fair_database/data/serializers.py index 99030aa5..eed3c587 100644 --- a/sasdata/fair_database/data/serializers.py +++ b/sasdata/fair_database/data/serializers.py @@ -1,3 +1,5 @@ +import os + from rest_framework import serializers from .models import Data @@ -8,7 +10,6 @@ class Meta: fields = "__all__" def validate(self, data): - print(data) - if not data['is_public'] and not data['current_user']: + if not self.context['is_public'] and not data['current_user']: raise serializers.ValidationError('private data must have an owner') return data \ No newline at end of file diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 9800dde5..42652ebb 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -74,13 +74,13 @@ def test_is_data_being_created(self): def test_is_data_being_created_no_user(self): file = open(find("cyl_400_40.txt"), 'rb') data = { - "is_public":False, + "is_public":True, "file":file } request = self.client2.post('/v1/data/upload/', data=data) self.assertEqual(request.status_code, status.HTTP_200_OK) self.assertEqual(request.data, {"current_user":'', "authenticated" : False, - "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) + "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : True}) Data.objects.get(id = 3).delete() # Test updating file diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 96d6e4e6..d807649a 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -58,9 +58,13 @@ def upload(request, data_id = None, version = None): db = Data.objects.get(pk = form.instance.pk) if request.user.is_authenticated: - serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path), "current_user" : request.user.id}) + serializer = DataSerializer(db, + data={"file_name":os.path.basename(form.instance.file.path), "current_user" : request.user.id}, + context={"is_public": db.is_public}) else: - serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path)}) + serializer = DataSerializer(db, + data={"file_name":os.path.basename(form.instance.file.path), "current_user": None}, + context={"is_public": db.is_public}) # updates file elif request.method == 'PUT': @@ -69,11 +73,11 @@ def upload(request, data_id = None, version = None): form = DataForm(request.data, request.FILES, instance=db) if form.is_valid(): form.save() - serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path)}, partial = True) + serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path), "current_user": request.user.id}, context={"is_public": db.is_public}, partial = True) else: return HttpResponseForbidden("user is not logged in") - if serializer.is_valid(): + if serializer.is_valid(raise_exception=True): serializer.save() #TODO get warnings/errors later return_data = {"current_user":request.user.username, "authenticated" : request.user.is_authenticated, "file_id" : db.id, "file_alternative_name":serializer.data["file_name"],"is_public" : serializer.data["is_public"]} From c4e47b11e4b2c96adbd57a795a422ee71a3d0161 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 10 Feb 2025 14:31:50 -0500 Subject: [PATCH 056/129] Rename Data model to DataFile --- sasdata/fair_database/data/admin.py | 4 +-- sasdata/fair_database/data/forms.py | 6 ++-- .../migrations/0002_rename_data_datafile.py | 19 +++++++++++++ sasdata/fair_database/data/models.py | 2 +- sasdata/fair_database/data/serializers.py | 8 ++---- sasdata/fair_database/data/tests.py | 20 ++++++------- sasdata/fair_database/data/views.py | 28 +++++++++---------- .../fair_database/test_permissions.py | 12 ++++---- 8 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 sasdata/fair_database/data/migrations/0002_rename_data_datafile.py diff --git a/sasdata/fair_database/data/admin.py b/sasdata/fair_database/data/admin.py index bfe8c7d9..7e4b7618 100644 --- a/sasdata/fair_database/data/admin.py +++ b/sasdata/fair_database/data/admin.py @@ -1,4 +1,4 @@ from django.contrib import admin -from .models import Data +from data.models import DataFile -admin.site.register(Data) \ No newline at end of file +admin.site.register(DataFile) \ No newline at end of file diff --git a/sasdata/fair_database/data/forms.py b/sasdata/fair_database/data/forms.py index e336efab..f49ffca1 100644 --- a/sasdata/fair_database/data/forms.py +++ b/sasdata/fair_database/data/forms.py @@ -1,8 +1,8 @@ from django import forms -from .models import Data +from data.models import DataFile # Create the form class. -class DataForm(forms.ModelForm): +class DataFileForm(forms.ModelForm): class Meta: - model = Data + model = DataFile fields = ["file", "is_public"] \ No newline at end of file diff --git a/sasdata/fair_database/data/migrations/0002_rename_data_datafile.py b/sasdata/fair_database/data/migrations/0002_rename_data_datafile.py new file mode 100644 index 00000000..33d9079b --- /dev/null +++ b/sasdata/fair_database/data/migrations/0002_rename_data_datafile.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.5 on 2025-02-10 19:30 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RenameModel( + old_name='Data', + new_name='DataFile', + ), + ] diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index e902193c..821f822d 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -3,7 +3,7 @@ from django.core.files.storage import FileSystemStorage # Create your models here. -class Data(models.Model): +class DataFile(models.Model): #username current_user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE) diff --git a/sasdata/fair_database/data/serializers.py b/sasdata/fair_database/data/serializers.py index eed3c587..70fed9d1 100644 --- a/sasdata/fair_database/data/serializers.py +++ b/sasdata/fair_database/data/serializers.py @@ -1,12 +1,10 @@ -import os - from rest_framework import serializers -from .models import Data +from data.models import DataFile -class DataSerializer(serializers.ModelSerializer): +class DataFileSerializer(serializers.ModelSerializer): class Meta: - model = Data + model = DataFile fields = "__all__" def validate(self, data): diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 42652ebb..8e1535b4 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -7,18 +7,18 @@ from rest_framework.test import APIClient, APITestCase from rest_framework import status -from data.models import Data +from data.models import DataFile def find(filename): return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) class TestLists(TestCase): def setUp(self): - public_test_data = Data.objects.create(id = 1, file_name = "cyl_400_40.txt", + public_test_data = DataFile.objects.create(id = 1, file_name = "cyl_400_40.txt", is_public = True) public_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) self.user = User.objects.create_user(username="testUser", password="secret", id = 2) - private_test_data = Data.objects.create(id = 3, current_user = self.user, + private_test_data = DataFile.objects.create(id = 3, current_user = self.user, file_name = "cyl_400_20.txt", is_public = False) private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) self.client = APIClient() @@ -50,7 +50,7 @@ def tearDown(self): class TestingDatabase(APITestCase): def setUp(self): self.user = User.objects.create_user(username="testUser", password="secret", id = 1) - self.data = Data.objects.create(id = 2, current_user = self.user, + self.data = DataFile.objects.create(id = 2, current_user = self.user, file_name = "cyl_400_20.txt", is_public = False) self.data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) self.client = APIClient() @@ -68,7 +68,7 @@ def test_is_data_being_created(self): self.assertEqual(request.status_code, status.HTTP_200_OK) self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) - Data.objects.get(id = 3).delete() + DataFile.objects.get(id = 3).delete() # Test data upload w/out authenticated user def test_is_data_being_created_no_user(self): @@ -81,7 +81,7 @@ def test_is_data_being_created_no_user(self): self.assertEqual(request.status_code, status.HTTP_200_OK) self.assertEqual(request.data, {"current_user":'', "authenticated" : False, "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : True}) - Data.objects.get(id = 3).delete() + DataFile.objects.get(id = 3).delete() # Test updating file def test_does_file_upload_update(self): @@ -93,10 +93,10 @@ def test_does_file_upload_update(self): request = self.client.put('/v1/data/upload/2/', data = data) self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, "file_id" : 2, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) - Data.objects.get(id = 2).delete() + DataFile.objects.get(id = 2).delete() def test_public_file_upload_update(self): - data_object = Data.objects.create(id=3, current_user=self.user, + data_object = DataFile.objects.create(id=3, current_user=self.user, file_name="cyl_testdata.txt", is_public=True) data_object.file.save("cyl_testdata.txt", open(find("cyl_testdata.txt"), 'rb')) file = open(find("cyl_testdata1.txt")) @@ -107,7 +107,7 @@ def test_public_file_upload_update(self): request = self.client.put('/v1/data/upload/3/', data=data) self.assertEqual(request.data, {"current_user": 'testUser', "authenticated": True, "file_id": 3, "file_alternative_name": "cyl_testdata1.txt", "is_public": True}) - Data.objects.get(id=3).delete() + DataFile.objects.get(id=3).delete() # Test file upload update fails when unauthorized def test_unauthorized_file_upload_update(self): @@ -118,7 +118,7 @@ def test_unauthorized_file_upload_update(self): } request = self.client2.put('/v1/data/upload/2/', data=data) self.assertEqual(request.status_code, status.HTTP_403_FORBIDDEN) - Data.objects.get(id=2).delete() + DataFile.objects.get(id=2).delete() # Test file download def test_does_download(self): diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index d807649a..a9d01b1e 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -6,9 +6,9 @@ from rest_framework.response import Response from sasdata.dataloader.loader import Loader -from .serializers import DataSerializer -from .models import Data -from .forms import DataForm +from data.serializers import DataFileSerializer +from data.models import DataFile +from data.forms import DataFileForm @api_view(['GET']) def list_data(request, username = None, version = None): @@ -16,13 +16,13 @@ def list_data(request, username = None, version = None): if username: data_list = {"user_data_ids": {}} if username == request.user.username and request.user.is_authenticated: - private_data = Data.objects.filter(current_user=request.user.id) + private_data = DataFile.objects.filter(current_user=request.user.id) for x in private_data: data_list["user_data_ids"][x.id] = x.file_name else: return HttpResponseBadRequest("user is not logged in, or username is not same as current user") else: - public_data = Data.objects.filter(is_public=True) + public_data = DataFile.objects.filter(is_public=True) data_list = {"public_data_ids": {}} for x in public_data: data_list["public_data_ids"][x.id] = x.file_name @@ -33,7 +33,7 @@ def list_data(request, username = None, version = None): def data_info(request, db_id, version = None): if request.method == 'GET': loader = Loader() - data_db = get_object_or_404(Data, id=db_id) + data_db = get_object_or_404(DataFile, id=db_id) if data_db.is_public: data_list = loader.load(data_db.file.path) contents = [str(data) for data in data_list] @@ -52,28 +52,28 @@ def data_info(request, db_id, version = None): def upload(request, data_id = None, version = None): # saves file if request.method in ['POST', 'PUT'] and data_id == None: - form = DataForm(request.data, request.FILES) + form = DataFileForm(request.data, request.FILES) if form.is_valid(): form.save() - db = Data.objects.get(pk = form.instance.pk) + db = DataFile.objects.get(pk = form.instance.pk) if request.user.is_authenticated: - serializer = DataSerializer(db, + serializer = DataFileSerializer(db, data={"file_name":os.path.basename(form.instance.file.path), "current_user" : request.user.id}, context={"is_public": db.is_public}) else: - serializer = DataSerializer(db, + serializer = DataFileSerializer(db, data={"file_name":os.path.basename(form.instance.file.path), "current_user": None}, context={"is_public": db.is_public}) # updates file elif request.method == 'PUT': if request.user.is_authenticated: - db = get_object_or_404(Data, current_user = request.user.id, id = data_id) - form = DataForm(request.data, request.FILES, instance=db) + db = get_object_or_404(DataFile, current_user = request.user.id, id = data_id) + form = DataFileForm(request.data, request.FILES, instance=db) if form.is_valid(): form.save() - serializer = DataSerializer(db, data={"file_name":os.path.basename(form.instance.file.path), "current_user": request.user.id}, context={"is_public": db.is_public}, partial = True) + serializer = DataFileSerializer(db, data={"file_name":os.path.basename(form.instance.file.path), "current_user": request.user.id}, context={"is_public": db.is_public}, partial = True) else: return HttpResponseForbidden("user is not logged in") @@ -87,7 +87,7 @@ def upload(request, data_id = None, version = None): @api_view(['GET']) def download(request, data_id, version = None): if request.method == 'GET': - data = get_object_or_404(Data, id=data_id) + data = get_object_or_404(DataFile, id=data_id) if not data.is_public: # add session key later if not request.user.is_authenticated: diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index ff62e5e0..1c37cd54 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -6,7 +6,7 @@ from rest_framework import status from rest_framework.test import APIClient, APITestCase -from data.models import Data +from data.models import DataFile def find(filename): return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) @@ -19,13 +19,13 @@ def setUp(self): email="email@domain.com") self.user2 = User.objects.create_user(username="testUser2", password="secret", id=2, email="email2@domain.com") - unowned_test_data = Data.objects.create(id=1, file_name="cyl_400_40.txt", + unowned_test_data = DataFile.objects.create(id=1, file_name="cyl_400_40.txt", is_public=True) unowned_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) - private_test_data = Data.objects.create(id=2, current_user=self.user, + private_test_data = DataFile.objects.create(id=2, current_user=self.user, file_name="cyl_400_20.txt", is_public=False) private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) - public_test_data = Data.objects.create(id=3, current_user=self.user, + public_test_data = DataFile.objects.create(id=3, current_user=self.user, file_name="cyl_testdata.txt", is_public=True) public_test_data.file.save("cyl_testdata.txt", open(find("cyl_testdata.txt"), 'rb')) self.login_data_1 = { @@ -107,7 +107,7 @@ def test_upload_authenticated(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {"current_user": 'testUser', "authenticated": True, "file_id": 4, "file_alternative_name": "cyl_testdata1.txt", "is_public": False}) - Data.objects.get(id=4).delete() + DataFile.objects.get(id=4).delete() # Unauthenticated user can upload public data only def test_upload_unauthenticated(self): @@ -143,7 +143,7 @@ def test_upload_put_authenticated(self): self.assertEqual(response2.data, {"current_user": 'testUser', "authenticated": True, "file_id": 3, "file_alternative_name": "cyl_testdata.txt", "is_public": False}) - Data.objects.get(id=3).is_public = True + DataFile.objects.get(id=3).is_public = True # Authenticated user cannot update unowned data def test_upload_put_unauthorized(self): From ddf2c3490575d6771082f6f18247972b1c37ff67 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 10 Feb 2025 14:55:35 -0500 Subject: [PATCH 057/129] Permission class for data --- sasdata/fair_database/data/permissions.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 sasdata/fair_database/data/permissions.py diff --git a/sasdata/fair_database/data/permissions.py b/sasdata/fair_database/data/permissions.py new file mode 100644 index 00000000..d0d32c2d --- /dev/null +++ b/sasdata/fair_database/data/permissions.py @@ -0,0 +1,15 @@ +from rest_framework import permissions + +def is_owner(request, obj): + return request.user.is_authenticated and request.user.id == obj.current_user + +class DataPermission(permissions.BasicPermission): + def has_object_permission(self, request, view, obj): + if request.method == 'GET': + if obj.is_public or is_owner(request, obj): + return True + elif request.method == 'DELETE': + if obj.is_private and is_owner(request, obj): + return True + else: + return is_owner(request, obj) \ No newline at end of file From f739281bf04942b041a44ece73e8a650c3cede11 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 10 Feb 2025 16:33:13 -0500 Subject: [PATCH 058/129] Create permissions class --- sasdata/fair_database/data/permissions.py | 15 ---------- .../fair_database/permissions.py | 28 +++++++++++++++++++ 2 files changed, 28 insertions(+), 15 deletions(-) delete mode 100644 sasdata/fair_database/data/permissions.py create mode 100644 sasdata/fair_database/fair_database/permissions.py diff --git a/sasdata/fair_database/data/permissions.py b/sasdata/fair_database/data/permissions.py deleted file mode 100644 index d0d32c2d..00000000 --- a/sasdata/fair_database/data/permissions.py +++ /dev/null @@ -1,15 +0,0 @@ -from rest_framework import permissions - -def is_owner(request, obj): - return request.user.is_authenticated and request.user.id == obj.current_user - -class DataPermission(permissions.BasicPermission): - def has_object_permission(self, request, view, obj): - if request.method == 'GET': - if obj.is_public or is_owner(request, obj): - return True - elif request.method == 'DELETE': - if obj.is_private and is_owner(request, obj): - return True - else: - return is_owner(request, obj) \ No newline at end of file diff --git a/sasdata/fair_database/fair_database/permissions.py b/sasdata/fair_database/fair_database/permissions.py new file mode 100644 index 00000000..51379b93 --- /dev/null +++ b/sasdata/fair_database/fair_database/permissions.py @@ -0,0 +1,28 @@ +from rest_framework.permissions import BasePermission + + +def is_owner(request, obj): + return request.user.is_authenticated and request.user.id == obj.current_user + +class DataPermission(BasePermission): + + def has_object_permission(self, request, view, obj): + if request.method == 'GET': + if obj.is_public or is_owner(request, obj): + return True + elif request.method == 'DELETE': + if obj.is_private and is_owner(request, obj): + return True + else: + return is_owner(request, obj) + +def check_permissions(request, obj): + if request.method == 'GET': + if obj.is_public or is_owner(request, obj): + return True + elif request.method == 'DELETE': + if obj.is_private and is_owner(request, obj): + return True + else: + return is_owner(request, obj) + From 6cdb989b4b28cd2076fff55fc03d6375927a53a2 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 10 Feb 2025 16:34:21 -0500 Subject: [PATCH 059/129] Change permissions class --- sasdata/fair_database/fair_database/settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index c96d7b93..22112af4 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -92,7 +92,13 @@ ) REST_FRAMEWORK = { - 'DEFAULT ATHENTICATION CLASSES': ('knox.auth.TokenAuthentication'), + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'knox.auth.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ], + #'DEFAULT_PERMISSION_CLASSES': [ + # 'fair_database.permissions.DataPermission', + #], } REST_AUTH = { From fcc429a14d556a897ae616507e855d1f3d0d3f0f Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 10 Feb 2025 16:36:45 -0500 Subject: [PATCH 060/129] Change error codes --- sasdata/fair_database/user_app/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index f019c29a..28ad0ab6 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -88,7 +88,7 @@ def test_user_put_name(self): # Test user info inaccessible when unauthenticated def test_user_unauthenticated(self): response = self.client.get('/auth/user/') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response.content, b'{"detail":"Authentication credentials were not provided."}') @@ -100,7 +100,7 @@ def test_login_logout(self): response2 = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') - self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response2.status_code, status.HTTP_401_UNAUTHORIZED) # Test logout is successful after registration def test_register_logout(self): @@ -109,7 +109,7 @@ def test_register_logout(self): response2 = self.client.get('/auth/user/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') - self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response2.status_code, status.HTTP_401_UNAUTHORIZED) def test_multiple_logout(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") From ace3dcebab56e846fbb037919b365e5e67faf711 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 10 Feb 2025 16:38:21 -0500 Subject: [PATCH 061/129] Remove relative imports --- sasdata/fair_database/user_app/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/user_app/views.py b/sasdata/fair_database/user_app/views.py index 88eb2eec..4a55fdc7 100644 --- a/sasdata/fair_database/user_app/views.py +++ b/sasdata/fair_database/user_app/views.py @@ -7,8 +7,8 @@ from allauth.account import app_settings as allauth_settings from allauth.socialaccount.providers.orcid.views import OrcidOAuth2Adapter -from .serializers import KnoxSerializer -from .util import create_knox_token +from user_app.serializers import KnoxSerializer +from user_app.util import create_knox_token #Login using knox tokens rather than django-rest-framework tokens. From 60b57207302fc76e286602d826fdaaa13106970a Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 10 Feb 2025 16:39:06 -0500 Subject: [PATCH 062/129] Start changing permissions checking --- sasdata/fair_database/data/views.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index a9d01b1e..7d80570b 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -9,22 +9,27 @@ from data.serializers import DataFileSerializer from data.models import DataFile from data.forms import DataFileForm +from fair_database import permissions @api_view(['GET']) def list_data(request, username = None, version = None): if request.method == 'GET': if username: data_list = {"user_data_ids": {}} - if username == request.user.username and request.user.is_authenticated: - private_data = DataFile.objects.filter(current_user=request.user.id) - for x in private_data: - data_list["user_data_ids"][x.id] = x.file_name - else: - return HttpResponseBadRequest("user is not logged in, or username is not same as current user") + #if username == request.user.username and request.user.is_authenticated: + private_data = DataFile.objects.filter(current_user=request.user.id) + for x in private_data: + if not permissions.check_permissions(request, x): + return HttpResponseForbidden() + data_list["user_data_ids"][x.id] = x.file_name + #else: + # return HttpResponseBadRequest("user is not logged in, or username is not same as current user") else: public_data = DataFile.objects.filter(is_public=True) data_list = {"public_data_ids": {}} for x in public_data: + if not permissions.check_permissions(request, x): + return HttpResponseForbidden() data_list["public_data_ids"][x.id] = x.file_name return Response(data_list) return HttpResponseBadRequest("not get method") From c8f5c519b12510f81ec013c6d3bb84bc01afb0fe Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 13:12:05 -0500 Subject: [PATCH 063/129] Change authentication serializer to used token directly --- sasdata/fair_database/user_app/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/user_app/serializers.py b/sasdata/fair_database/user_app/serializers.py index 5181a735..7d315377 100644 --- a/sasdata/fair_database/user_app/serializers.py +++ b/sasdata/fair_database/user_app/serializers.py @@ -7,8 +7,8 @@ class KnoxSerializer(serializers.Serializer): """ Serializer for Knox authentication. """ - token = serializers.CharField() + token = serializers.SerializerMethodField() user = UserDetailsSerializer() def get_token(self, obj): - return obj["token"][1] \ No newline at end of file + return obj["token"][1] \ No newline at end of file From 9c1490b687b0804ebaffbc32584862f4d800f418 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 13:13:44 -0500 Subject: [PATCH 064/129] Change tests to use token authentication properly --- .../fair_database/test_permissions.py | 61 ++++++++++--------- sasdata/fair_database/user_app/tests.py | 17 +++--- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index 1c37cd54..c9c2e991 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -11,6 +11,9 @@ def find(filename): return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) +def auth_header(response): + return {'Authorization': 'Token ' + response.data['token']} + class DataListPermissionsTests(APITestCase): ''' Test permissions of data views using user_app for authentication. ''' @@ -42,9 +45,9 @@ def setUp(self): # Authenticated user can view list of data # TODO: change to reflect inclusion of owned private data def test_list_authenticated(self): - self.client.post('/auth/login/', data=self.login_data_1) - response = self.client.get('/v1/data/list/') - response2 = self.client.get('/v1/data/list/testUser/') + token = self.client.post('/auth/login/', data=self.login_data_1) + response = self.client.get('/v1/data/list/', headers=auth_header(token)) + response2 = self.client.get('/v1/data/list/testUser/', headers=auth_header(token)) self.assertEqual(response.data, {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) self.assertEqual(response2.data, @@ -53,10 +56,10 @@ def test_list_authenticated(self): # Authenticated user cannot view other users' private data on list # TODO: Change response codes def test_list_authenticated_2(self): - self.client.post('/auth/login/', data=self.login_data_2) - response = self.client.get('/v1/data/list/') - response2 = self.client.get('/v1/data/list/testUser/') - response3 = self.client.get('/v1/data/list/testUser2/') + token = self.client.post('/auth/login/', data=self.login_data_2) + response = self.client.get('/v1/data/list/', headers=auth_header(token)) + response2 = self.client.get('/v1/data/list/testUser/', headers=auth_header(token)) + response3 = self.client.get('/v1/data/list/testUser2/', headers=auth_header(token)) self.assertEqual(response.data, {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) @@ -72,17 +75,17 @@ def test_list_unauthenticated(self): # Authenticated user can load public data and owned private data def test_load_authenticated(self): - self.client.post('/auth/login/', data=self.login_data_1) - response = self.client.get('/v1/data/load/1/') - response2 = self.client.get('/v1/data/load/2/') + token = self.client.post('/auth/login/', data=self.login_data_1) + response = self.client.get('/v1/data/load/1/', headers=auth_header(token)) + response2 = self.client.get('/v1/data/load/2/', headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Authenticated user cannot load others' private data def test_load_unauthorized(self): - self.client.post('/auth/login/', data=self.login_data_2) - response = self.client.get('/v1/data/load/2/') - response2 = self.client.get('/v1/data/load/3/') + token = self.client.post('/auth/login/', data=self.login_data_2) + response = self.client.get('/v1/data/load/2/', headers=auth_header(token)) + response2 = self.client.get('/v1/data/load/3/', headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response2.status_code, status.HTTP_200_OK) @@ -97,13 +100,13 @@ def test_load_unauthenticated(self): # Authenticated user can upload data def test_upload_authenticated(self): - self.client.post('/auth/login/', data=self.login_data_1) + token = self.client.post('/auth/login/', data=self.login_data_1) file = open(find('cyl_testdata1.txt'), 'rb') data = { 'file': file, 'is_public': False } - response = self.client.post('/v1/data/upload/', data=data) + response = self.client.post('/v1/data/upload/', data=data, headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {"current_user": 'testUser', "authenticated": True, "file_id": 4, "file_alternative_name": "cyl_testdata1.txt", "is_public": False}) @@ -131,12 +134,12 @@ def test_upload_unauthenticated(self): # Authenticated user can update own data def test_upload_put_authenticated(self): - self.client.post('/auth/login/', data=self.login_data_1) + token = self.client.post('/auth/login/', data=self.login_data_1) data = { "is_public": False } - response = self.client.put('/v1/data/upload/2/', data=data) - response2 = self.client.put('/v1/data/upload/3/', data=data) + response = self.client.put('/v1/data/upload/2/', data=data, headers=auth_header(token)) + response2 = self.client.put('/v1/data/upload/3/', data=data, headers=auth_header(token)) self.assertEqual(response.data, {"current_user": 'testUser', "authenticated": True, "file_id": 2, "file_alternative_name": "cyl_400_20.txt", "is_public": False}) @@ -147,15 +150,15 @@ def test_upload_put_authenticated(self): # Authenticated user cannot update unowned data def test_upload_put_unauthorized(self): - self.client.post('/auth/login/', data=self.login_data_2) + token = self.client.post('/auth/login/', data=self.login_data_2) file = open(find("cyl_400_40.txt")) data = { "file": file, "is_public": False } - response = self.client.put('/v1/data/upload/1/', data=data) - response2 = self.client.put('/v1/data/upload/2/', data=data) - response3 = self.client.put('/v1/data/upload/3/', data=data) + response = self.client.put('/v1/data/upload/1/', data=data, headers=auth_header(token)) + response2 = self.client.put('/v1/data/upload/2/', data=data, headers=auth_header(token)) + response3 = self.client.put('/v1/data/upload/3/', data=data, headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response2.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response3.status_code, status.HTTP_404_NOT_FOUND) @@ -176,19 +179,19 @@ def test_upload_put_unauthenticated(self): # Authenticated user can download public and own data def test_download_authenticated(self): - self.client.post('/auth/login/', data=self.login_data_1) - response = self.client.get('/v1/data/1/download/') - response2 = self.client.get('/v1/data/2/download/') - response3 = self.client.get('/v1/data/3/download/') + token = self.client.post('/auth/login/', data=self.login_data_1) + response = self.client.get('/v1/data/1/download/', headers=auth_header(token)) + response2 = self.client.get('/v1/data/2/download/', headers=auth_header(token)) + response3 = self.client.get('/v1/data/3/download/', headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) self.assertEqual(response3.status_code, status.HTTP_200_OK) # Authenticated user cannot download others' data def test_download_unauthorized(self): - self.client.post('/auth/login/', data=self.login_data_2) - response = self.client.get('/v1/data/2/download/') - response2 = self.client.get('/v1/data/3/download/') + token = self.client.post('/auth/login/', data=self.login_data_2) + response = self.client.get('/v1/data/2/download/', headers=auth_header(token)) + response2 = self.client.get('/v1/data/3/download/', headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response2.status_code, status.HTTP_200_OK) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index 28ad0ab6..b3f203a3 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -1,5 +1,3 @@ -import requests - from django.test import TestCase from rest_framework import status from rest_framework.test import APIClient @@ -23,11 +21,14 @@ def setUp(self): "password": "sasview!" } + def auth_header(self, response): + return {'Authorization': 'Token ' + response.data['token']} + # Test if registration successfully creates a new user and logs in def test_register(self): response = self.client.post('/auth/register/',data=self.register_data) user = User.objects.get(username="testUser") - response2 = self.client.get('/auth/user/') + response2 = self.client.get('/auth/user/', headers=self.auth_header(response)) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(user.email, self.register_data["email"]) self.assertEqual(response2.status_code, status.HTTP_200_OK) @@ -36,7 +37,7 @@ def test_register(self): def test_login(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") response = self.client.post('/auth/login/', data=self.login_data) - response2 = self.client.get('/auth/user/') + response2 = self.client.get('/auth/user/', headers=self.auth_header(response)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) @@ -115,9 +116,9 @@ def test_multiple_logout(self): User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") client2 = APIClient() self.client.post('/auth/login/', data=self.login_data) - client2.post('/auth/login/', data=self.login_data) + token = client2.post('/auth/login/', data=self.login_data) response = self.client.post('/auth/logout/') - response2 = client2.get('/auth/user/') + response2 = client2.get('/auth/user/', headers=self.auth_header(token)) response3 = client2.post('/auth/logout/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) @@ -134,7 +135,7 @@ def test_register_login(self): # Test password is successfully changed def test_password_change(self): - self.client.post('/auth/register/', data=self.register_data) + token = self.client.post('/auth/register/', data=self.register_data) data = { "new_password1": "sasview?", "new_password2": "sasview?", @@ -142,7 +143,7 @@ def test_password_change(self): } l_data = self.login_data l_data["password"] = "sasview?" - response = self.client.post('/auth/password/change/', data=data) + response = self.client.post('/auth/password/change/', data=data, headers=self.auth_header(token)) logout_response = self.client.post('/auth/logout/') login_response = self.client.post('/auth/login/', data=l_data) self.assertEqual(response.status_code, status.HTTP_200_OK) From e2484e5873516e6899a9d3658ac463d48ee0e486 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 13:14:51 -0500 Subject: [PATCH 065/129] Turn off session authentication option --- sasdata/fair_database/fair_database/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 22112af4..dd448c8e 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -94,7 +94,7 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'knox.auth.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', + #'rest_framework.authentication.SessionAuthentication', ], #'DEFAULT_PERMISSION_CLASSES': [ # 'fair_database.permissions.DataPermission', From b3426a1f0ad3e0813a538e71a99fdb6f6cc6d283 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 14:25:13 -0500 Subject: [PATCH 066/129] Add django tests to github actions and formating config --- .github/workflows/test-fair-database.yml | 56 ++++++++++++++++++++++++ .pre-commit-config.yaml | 19 ++++++++ 2 files changed, 75 insertions(+) create mode 100644 .github/workflows/test-fair-database.yml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/test-fair-database.yml b/.github/workflows/test-fair-database.yml new file mode 100644 index 00000000..531c93cf --- /dev/null +++ b/.github/workflows/test-fair-database.yml @@ -0,0 +1,56 @@ +name: Tests + +on: + [push, pull_request] + +defaults: + run: + shell: bash + +jobs: + unit-test: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: ['3.12'] + fail-fast: false + + steps: + + - name: Obtain SasData source from git + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: | + **/test.yml + **/requirements*.txt + + ### Installation of build-dependencies + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install wheel setuptools + python -m pip install -r requirements.txt + python -m pip install -r sasdata/fair_database/requirements.txt + + ### Build and test sasdata + + - name: Build sasdata + run: | + # BUILD SASDATA + python setup.py clean + python setup.py build + python -m pip install . + + ### Build documentation (if enabled) + + - name: Test with Django tests + run: | + python sasdata/fair_database/manage.py test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..f1958b33 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + files: "sasdata/fair_database/.*" +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.2 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + files: "sasdata/fair_database/.*" + - id: ruff-format + files: "sasdata/fair_database/.*" +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + files: "sasdata/fair_database/.*" From 2ac69546117bd1e66b7673b711adebc9cc31f3f1 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 14:49:06 -0500 Subject: [PATCH 067/129] Restore checks for user-specific list data --- sasdata/fair_database/data/views.py | 109 ++++++++++++++++++---------- 1 file changed, 72 insertions(+), 37 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 7d80570b..1a3f0d2c 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -1,7 +1,12 @@ import os from django.shortcuts import get_object_or_404 -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404, FileResponse +from django.http import ( + HttpResponseBadRequest, + HttpResponseForbidden, + Http404, + FileResponse, +) from rest_framework.decorators import api_view from rest_framework.response import Response @@ -11,19 +16,20 @@ from data.forms import DataFileForm from fair_database import permissions -@api_view(['GET']) -def list_data(request, username = None, version = None): - if request.method == 'GET': + +@api_view(["GET"]) +def list_data(request, username=None, version=None): + if request.method == "GET": if username: data_list = {"user_data_ids": {}} - #if username == request.user.username and request.user.is_authenticated: - private_data = DataFile.objects.filter(current_user=request.user.id) - for x in private_data: - if not permissions.check_permissions(request, x): - return HttpResponseForbidden() - data_list["user_data_ids"][x.id] = x.file_name - #else: - # return HttpResponseBadRequest("user is not logged in, or username is not same as current user") + if username == request.user.username and request.user.is_authenticated: + private_data = DataFile.objects.filter(current_user=request.user.id) + for x in private_data: + data_list["user_data_ids"][x.id] = x.file_name + else: + return HttpResponseBadRequest( + "user is not logged in, or username is not same as current user" + ) else: public_data = DataFile.objects.filter(is_public=True) data_list = {"public_data_ids": {}} @@ -34,9 +40,10 @@ def list_data(request, username = None, version = None): return Response(data_list) return HttpResponseBadRequest("not get method") -@api_view(['GET']) -def data_info(request, db_id, version = None): - if request.method == 'GET': + +@api_view(["GET"]) +def data_info(request, db_id, version=None): + if request.method == "GET": loader = Loader() data_db = get_object_or_404(DataFile, id=db_id) if data_db.is_public: @@ -49,49 +56,77 @@ def data_info(request, db_id, version = None): contents = [str(data) for data in data_list] return_data = {data_db.file_name: contents} else: - return HttpResponseBadRequest("Database is either not public or wrong auth token") + return HttpResponseBadRequest( + "Database is either not public or wrong auth token" + ) return Response(return_data) return HttpResponseBadRequest() -@api_view(['POST', 'PUT']) -def upload(request, data_id = None, version = None): + +@api_view(["POST", "PUT"]) +def upload(request, data_id=None, version=None): # saves file - if request.method in ['POST', 'PUT'] and data_id == None: + if request.method in ["POST", "PUT"] and data_id is None: form = DataFileForm(request.data, request.FILES) if form.is_valid(): form.save() - db = DataFile.objects.get(pk = form.instance.pk) + db = DataFile.objects.get(pk=form.instance.pk) if request.user.is_authenticated: - serializer = DataFileSerializer(db, - data={"file_name":os.path.basename(form.instance.file.path), "current_user" : request.user.id}, - context={"is_public": db.is_public}) + serializer = DataFileSerializer( + db, + data={ + "file_name": os.path.basename(form.instance.file.path), + "current_user": request.user.id, + }, + context={"is_public": db.is_public}, + ) else: - serializer = DataFileSerializer(db, - data={"file_name":os.path.basename(form.instance.file.path), "current_user": None}, - context={"is_public": db.is_public}) + serializer = DataFileSerializer( + db, + data={ + "file_name": os.path.basename(form.instance.file.path), + "current_user": None, + }, + context={"is_public": db.is_public}, + ) # updates file - elif request.method == 'PUT': + elif request.method == "PUT": if request.user.is_authenticated: - db = get_object_or_404(DataFile, current_user = request.user.id, id = data_id) + db = get_object_or_404(DataFile, current_user=request.user.id, id=data_id) form = DataFileForm(request.data, request.FILES, instance=db) if form.is_valid(): form.save() - serializer = DataFileSerializer(db, data={"file_name":os.path.basename(form.instance.file.path), "current_user": request.user.id}, context={"is_public": db.is_public}, partial = True) + serializer = DataFileSerializer( + db, + data={ + "file_name": os.path.basename(form.instance.file.path), + "current_user": request.user.id, + }, + context={"is_public": db.is_public}, + partial=True, + ) else: return HttpResponseForbidden("user is not logged in") if serializer.is_valid(raise_exception=True): serializer.save() - #TODO get warnings/errors later - return_data = {"current_user":request.user.username, "authenticated" : request.user.is_authenticated, "file_id" : db.id, "file_alternative_name":serializer.data["file_name"],"is_public" : serializer.data["is_public"]} + # TODO get warnings/errors later + return_data = { + "current_user": request.user.username, + "authenticated": request.user.is_authenticated, + "file_id": db.id, + "file_alternative_name": serializer.data["file_name"], + "is_public": serializer.data["is_public"], + } return Response(return_data) -#downloads a file -@api_view(['GET']) -def download(request, data_id, version = None): - if request.method == 'GET': + +# downloads a file +@api_view(["GET"]) +def download(request, data_id, version=None): + if request.method == "GET": data = get_object_or_404(DataFile, id=data_id) if not data.is_public: # add session key later @@ -101,10 +136,10 @@ def download(request, data_id, version = None): return HttpResponseForbidden("data is private") # TODO add issues later try: - file = open(data.file.path, 'rb') + file = open(data.file.path, "rb") except Exception as e: return HttpResponseBadRequest(str(e)) if file is None: raise Http404("File not found.") return FileResponse(file, as_attachment=True) - return HttpResponseBadRequest() \ No newline at end of file + return HttpResponseBadRequest() From 0a853cb89239709027652af700b534225d2f379e Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 14:50:18 -0500 Subject: [PATCH 068/129] ruff formatting --- sasdata/fair_database/data/admin.py | 2 +- sasdata/fair_database/data/apps.py | 4 +- sasdata/fair_database/data/forms.py | 3 +- .../data/migrations/0001_initial.py | 52 +++- .../migrations/0002_rename_data_datafile.py | 7 +- sasdata/fair_database/data/models.py | 35 ++- sasdata/fair_database/data/serializers.py | 7 +- sasdata/fair_database/data/tests.py | 160 ++++++---- sasdata/fair_database/data/urls.py | 15 +- sasdata/fair_database/fair_database/asgi.py | 2 +- .../fair_database/permissions.py | 12 +- .../fair_database/fair_database/settings.py | 148 +++++----- .../fair_database/test_permissions.py | 276 +++++++++++------- .../fair_database/upload_example_data.py | 15 +- sasdata/fair_database/fair_database/urls.py | 5 +- sasdata/fair_database/fair_database/wsgi.py | 2 +- sasdata/fair_database/manage.py | 5 +- sasdata/fair_database/user_app/admin.py | 2 - sasdata/fair_database/user_app/apps.py | 4 +- sasdata/fair_database/user_app/models.py | 2 - sasdata/fair_database/user_app/serializers.py | 3 +- sasdata/fair_database/user_app/tests.py | 131 +++++---- sasdata/fair_database/user_app/urls.py | 19 +- sasdata/fair_database/user_app/util.py | 2 +- sasdata/fair_database/user_app/views.py | 26 +- 25 files changed, 544 insertions(+), 395 deletions(-) diff --git a/sasdata/fair_database/data/admin.py b/sasdata/fair_database/data/admin.py index 7e4b7618..e000e532 100644 --- a/sasdata/fair_database/data/admin.py +++ b/sasdata/fair_database/data/admin.py @@ -1,4 +1,4 @@ from django.contrib import admin from data.models import DataFile -admin.site.register(DataFile) \ No newline at end of file +admin.site.register(DataFile) diff --git a/sasdata/fair_database/data/apps.py b/sasdata/fair_database/data/apps.py index f6b7ef7f..b882be95 100644 --- a/sasdata/fair_database/data/apps.py +++ b/sasdata/fair_database/data/apps.py @@ -2,5 +2,5 @@ class DataConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'data' + default_auto_field = "django.db.models.BigAutoField" + name = "data" diff --git a/sasdata/fair_database/data/forms.py b/sasdata/fair_database/data/forms.py index f49ffca1..fde1813d 100644 --- a/sasdata/fair_database/data/forms.py +++ b/sasdata/fair_database/data/forms.py @@ -1,8 +1,9 @@ from django import forms from data.models import DataFile + # Create the form class. class DataFileForm(forms.ModelForm): class Meta: model = DataFile - fields = ["file", "is_public"] \ No newline at end of file + fields = ["file", "is_public"] diff --git a/sasdata/fair_database/data/migrations/0001_initial.py b/sasdata/fair_database/data/migrations/0001_initial.py index 1c7c9df3..ce7cee7d 100644 --- a/sasdata/fair_database/data/migrations/0001_initial.py +++ b/sasdata/fair_database/data/migrations/0001_initial.py @@ -7,7 +7,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -16,13 +15,52 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Data', + name="Data", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('file_name', models.CharField(blank=True, default=None, help_text='File name', max_length=200, null=True)), - ('file', models.FileField(default=None, help_text='This is a file', storage=django.core.files.storage.FileSystemStorage(), upload_to='uploaded_files')), - ('is_public', models.BooleanField(default=False, help_text='opt in to submit your data into example pool')), - ('current_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "file_name", + models.CharField( + blank=True, + default=None, + help_text="File name", + max_length=200, + null=True, + ), + ), + ( + "file", + models.FileField( + default=None, + help_text="This is a file", + storage=django.core.files.storage.FileSystemStorage(), + upload_to="uploaded_files", + ), + ), + ( + "is_public", + models.BooleanField( + default=False, + help_text="opt in to submit your data into example pool", + ), + ), + ( + "current_user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/sasdata/fair_database/data/migrations/0002_rename_data_datafile.py b/sasdata/fair_database/data/migrations/0002_rename_data_datafile.py index 33d9079b..80a25548 100644 --- a/sasdata/fair_database/data/migrations/0002_rename_data_datafile.py +++ b/sasdata/fair_database/data/migrations/0002_rename_data_datafile.py @@ -5,15 +5,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('data', '0001_initial'), + ("data", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.RenameModel( - old_name='Data', - new_name='DataFile', + old_name="Data", + new_name="DataFile", ), ] diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 821f822d..013ef0a9 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -2,21 +2,30 @@ from django.contrib.auth.models import User from django.core.files.storage import FileSystemStorage + # Create your models here. class DataFile(models.Model): - #username - current_user = models.ForeignKey(User, blank=True, - null=True, on_delete=models.CASCADE) + # username + current_user = models.ForeignKey( + User, blank=True, null=True, on_delete=models.CASCADE + ) - #file name - file_name = models.CharField(max_length=200, default=None, - blank=True, null=True, help_text="File name") + # file name + file_name = models.CharField( + max_length=200, default=None, blank=True, null=True, help_text="File name" + ) - #imported data - #user can either import a file path or actual file - file = models.FileField(blank=False, default=None, help_text="This is a file", - upload_to="uploaded_files", storage=FileSystemStorage()) + # imported data + # user can either import a file path or actual file + file = models.FileField( + blank=False, + default=None, + help_text="This is a file", + upload_to="uploaded_files", + storage=FileSystemStorage(), + ) - #is the data public? - is_public = models.BooleanField(default=False, - help_text= "opt in to submit your data into example pool") \ No newline at end of file + # is the data public? + is_public = models.BooleanField( + default=False, help_text="opt in to submit your data into example pool" + ) diff --git a/sasdata/fair_database/data/serializers.py b/sasdata/fair_database/data/serializers.py index 70fed9d1..94c1f4e4 100644 --- a/sasdata/fair_database/data/serializers.py +++ b/sasdata/fair_database/data/serializers.py @@ -2,12 +2,13 @@ from data.models import DataFile + class DataFileSerializer(serializers.ModelSerializer): class Meta: model = DataFile fields = "__all__" def validate(self, data): - if not self.context['is_public'] and not data['current_user']: - raise serializers.ValidationError('private data must have an owner') - return data \ No newline at end of file + if not self.context["is_public"] and not data["current_user"]: + raise serializers.ValidationError("private data must have an owner") + return data diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 8e1535b4..28243173 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -9,129 +9,161 @@ from data.models import DataFile + def find(filename): - return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) + return os.path.join( + os.path.dirname(__file__), "../../example_data/1d_data", filename + ) + class TestLists(TestCase): def setUp(self): - public_test_data = DataFile.objects.create(id = 1, file_name = "cyl_400_40.txt", - is_public = True) - public_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) - self.user = User.objects.create_user(username="testUser", password="secret", id = 2) - private_test_data = DataFile.objects.create(id = 3, current_user = self.user, - file_name = "cyl_400_20.txt", is_public = False) - private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) + public_test_data = DataFile.objects.create( + id=1, file_name="cyl_400_40.txt", is_public=True + ) + public_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), "rb")) + self.user = User.objects.create_user( + username="testUser", password="secret", id=2 + ) + private_test_data = DataFile.objects.create( + id=3, current_user=self.user, file_name="cyl_400_20.txt", is_public=False + ) + private_test_data.file.save( + "cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb") + ) self.client = APIClient() self.client.force_authenticate(user=self.user) # Test list public data def test_does_list_public(self): - request = self.client.get('/v1/data/list/') - self.assertEqual(request.data, {"public_data_ids":{1:"cyl_400_40.txt"}}) + request = self.client.get("/v1/data/list/") + self.assertEqual(request.data, {"public_data_ids": {1: "cyl_400_40.txt"}}) # Test list a user's private data def test_does_list_user(self): - request = self.client.get('/v1/data/list/testUser/', user = self.user) - self.assertEqual(request.data, {"user_data_ids":{3:"cyl_400_20.txt"}}) + request = self.client.get("/v1/data/list/testUser/", user=self.user) + self.assertEqual(request.data, {"user_data_ids": {3: "cyl_400_20.txt"}}) # Test loading a public data file def test_does_load_data_info_public(self): - request = self.client.get('/v1/data/load/1/') + request = self.client.get("/v1/data/load/1/") self.assertEqual(request.status_code, status.HTTP_200_OK) # Test loading private data with authorization def test_does_load_data_info_private(self): - request = self.client.get('/v1/data/load/3/') + request = self.client.get("/v1/data/load/3/") self.assertEqual(request.status_code, status.HTTP_200_OK) def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) + class TestingDatabase(APITestCase): def setUp(self): - self.user = User.objects.create_user(username="testUser", password="secret", id = 1) - self.data = DataFile.objects.create(id = 2, current_user = self.user, - file_name = "cyl_400_20.txt", is_public = False) - self.data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) + self.user = User.objects.create_user( + username="testUser", password="secret", id=1 + ) + self.data = DataFile.objects.create( + id=2, current_user=self.user, file_name="cyl_400_20.txt", is_public=False + ) + self.data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb")) self.client = APIClient() self.client.force_authenticate(user=self.user) self.client2 = APIClient() # Test data upload creates data in database def test_is_data_being_created(self): - file = open(find("cyl_400_40.txt"), 'rb') - data = { - "is_public":False, - "file":file - } - request = self.client.post('/v1/data/upload/', data=data) + file = open(find("cyl_400_40.txt"), "rb") + data = {"is_public": False, "file": file} + request = self.client.post("/v1/data/upload/", data=data) self.assertEqual(request.status_code, status.HTTP_200_OK) - self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, - "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) - DataFile.objects.get(id = 3).delete() + self.assertEqual( + request.data, + { + "current_user": "testUser", + "authenticated": True, + "file_id": 3, + "file_alternative_name": "cyl_400_40.txt", + "is_public": False, + }, + ) + DataFile.objects.get(id=3).delete() # Test data upload w/out authenticated user def test_is_data_being_created_no_user(self): - file = open(find("cyl_400_40.txt"), 'rb') - data = { - "is_public":True, - "file":file - } - request = self.client2.post('/v1/data/upload/', data=data) + file = open(find("cyl_400_40.txt"), "rb") + data = {"is_public": True, "file": file} + request = self.client2.post("/v1/data/upload/", data=data) self.assertEqual(request.status_code, status.HTTP_200_OK) - self.assertEqual(request.data, {"current_user":'', "authenticated" : False, - "file_id" : 3, "file_alternative_name":"cyl_400_40.txt","is_public" : True}) - DataFile.objects.get(id = 3).delete() + self.assertEqual( + request.data, + { + "current_user": "", + "authenticated": False, + "file_id": 3, + "file_alternative_name": "cyl_400_40.txt", + "is_public": True, + }, + ) + DataFile.objects.get(id=3).delete() # Test updating file def test_does_file_upload_update(self): file = open(find("cyl_400_40.txt")) - data = { - "file":file, - "is_public":False - } - request = self.client.put('/v1/data/upload/2/', data = data) - self.assertEqual(request.data, {"current_user":'testUser', "authenticated" : True, - "file_id" : 2, "file_alternative_name":"cyl_400_40.txt","is_public" : False}) - DataFile.objects.get(id = 2).delete() + data = {"file": file, "is_public": False} + request = self.client.put("/v1/data/upload/2/", data=data) + self.assertEqual( + request.data, + { + "current_user": "testUser", + "authenticated": True, + "file_id": 2, + "file_alternative_name": "cyl_400_40.txt", + "is_public": False, + }, + ) + DataFile.objects.get(id=2).delete() def test_public_file_upload_update(self): - data_object = DataFile.objects.create(id=3, current_user=self.user, - file_name="cyl_testdata.txt", is_public=True) - data_object.file.save("cyl_testdata.txt", open(find("cyl_testdata.txt"), 'rb')) + data_object = DataFile.objects.create( + id=3, current_user=self.user, file_name="cyl_testdata.txt", is_public=True + ) + data_object.file.save("cyl_testdata.txt", open(find("cyl_testdata.txt"), "rb")) file = open(find("cyl_testdata1.txt")) - data = { - "file": file, - "is_public": True - } - request = self.client.put('/v1/data/upload/3/', data=data) - self.assertEqual(request.data, {"current_user": 'testUser', "authenticated": True, - "file_id": 3, "file_alternative_name": "cyl_testdata1.txt", "is_public": True}) + data = {"file": file, "is_public": True} + request = self.client.put("/v1/data/upload/3/", data=data) + self.assertEqual( + request.data, + { + "current_user": "testUser", + "authenticated": True, + "file_id": 3, + "file_alternative_name": "cyl_testdata1.txt", + "is_public": True, + }, + ) DataFile.objects.get(id=3).delete() # Test file upload update fails when unauthorized def test_unauthorized_file_upload_update(self): file = open(find("cyl_400_40.txt")) - data = { - "file": file, - "is_public": False - } - request = self.client2.put('/v1/data/upload/2/', data=data) + data = {"file": file, "is_public": False} + request = self.client2.put("/v1/data/upload/2/", data=data) self.assertEqual(request.status_code, status.HTTP_403_FORBIDDEN) DataFile.objects.get(id=2).delete() # Test file download def test_does_download(self): - request = self.client.get('/v1/data/2/download/') - file_contents = b''.join(request.streaming_content) - test_file = open(find('cyl_400_20.txt'), 'rb') + request = self.client.get("/v1/data/2/download/") + file_contents = b"".join(request.streaming_content) + test_file = open(find("cyl_400_20.txt"), "rb") self.assertEqual(request.status_code, status.HTTP_200_OK) self.assertEqual(file_contents, test_file.read()) # Test file download fails when unauthorized def test_unauthorized_download(self): - request2 = self.client2.get('/v1/data/2/download/') + request2 = self.client2.get("/v1/data/2/download/") self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) def tearDown(self): - shutil.rmtree(settings.MEDIA_ROOT) \ No newline at end of file + shutil.rmtree(settings.MEDIA_ROOT) diff --git a/sasdata/fair_database/data/urls.py b/sasdata/fair_database/data/urls.py index abe4ffdb..17fa2adf 100644 --- a/sasdata/fair_database/data/urls.py +++ b/sasdata/fair_database/data/urls.py @@ -3,11 +3,10 @@ from . import views urlpatterns = [ - path("list/", views.list_data, name = "list public file_ids"), - path("list/<str:username>/", views.list_data, name = "view users file_ids"), - path("load/<int:db_id>/", views.data_info, name = "views data using file id"), - - path("upload/", views.upload, name = "upload data into db"), - path("upload/<data_id>/", views.upload, name = "update file in data"), - path("<int:data_id>/download/", views.download, name = "download data from db"), -] \ No newline at end of file + path("list/", views.list_data, name="list public file_ids"), + path("list/<str:username>/", views.list_data, name="view users file_ids"), + path("load/<int:db_id>/", views.data_info, name="views data using file id"), + path("upload/", views.upload, name="upload data into db"), + path("upload/<data_id>/", views.upload, name="update file in data"), + path("<int:data_id>/download/", views.download, name="download data from db"), +] diff --git a/sasdata/fair_database/fair_database/asgi.py b/sasdata/fair_database/fair_database/asgi.py index f47618a3..a10c9b21 100644 --- a/sasdata/fair_database/fair_database/asgi.py +++ b/sasdata/fair_database/fair_database/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fair_database.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fair_database.settings") application = get_asgi_application() diff --git a/sasdata/fair_database/fair_database/permissions.py b/sasdata/fair_database/fair_database/permissions.py index 51379b93..fb194938 100644 --- a/sasdata/fair_database/fair_database/permissions.py +++ b/sasdata/fair_database/fair_database/permissions.py @@ -4,25 +4,25 @@ def is_owner(request, obj): return request.user.is_authenticated and request.user.id == obj.current_user -class DataPermission(BasePermission): +class DataPermission(BasePermission): def has_object_permission(self, request, view, obj): - if request.method == 'GET': + if request.method == "GET": if obj.is_public or is_owner(request, obj): return True - elif request.method == 'DELETE': + elif request.method == "DELETE": if obj.is_private and is_owner(request, obj): return True else: return is_owner(request, obj) + def check_permissions(request, obj): - if request.method == 'GET': + if request.method == "GET": if obj.is_public or is_owner(request, obj): return True - elif request.method == 'DELETE': + elif request.method == "DELETE": if obj.is_private and is_owner(request, obj): return True else: return is_owner(request, obj) - diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index dd448c8e..885e3c90 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -21,7 +21,7 @@ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure--f-t5!pdhq&4)^&xenr^k0e8n%-h06jx9d0&2kft(!+1$xzig)' +SECRET_KEY = "django-insecure--f-t5!pdhq&4)^&xenr^k0e8n%-h06jx9d0&2kft(!+1$xzig)" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -32,107 +32,105 @@ # Application definition INSTALLED_APPS = [ - 'data.apps.DataConfig', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'rest_framework', - 'rest_framework.authtoken', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'allauth.socialaccount.providers.orcid', - 'dj_rest_auth', - 'dj_rest_auth.registration', - 'knox', - 'user_app.apps.UserAppConfig', + "data.apps.DataConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.sites", + "rest_framework", + "rest_framework.authtoken", + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.orcid", + "dj_rest_auth", + "dj_rest_auth.registration", + "knox", + "user_app.apps.UserAppConfig", ] SITE_ID = 1 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', - 'allauth.account.middleware.AccountMiddleware', + "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", + "allauth.account.middleware.AccountMiddleware", ] -ROOT_URLCONF = 'fair_database.urls' +ROOT_URLCONF = "fair_database.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - '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', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "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", ], }, }, ] -WSGI_APPLICATION = 'fair_database.wsgi.application' +WSGI_APPLICATION = "fair_database.wsgi.application" # Authentication AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'allauth.account.auth_backends.AuthenticationBackend', + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", ) REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'knox.auth.TokenAuthentication', + "DEFAULT_AUTHENTICATION_CLASSES": [ + "knox.auth.TokenAuthentication", #'rest_framework.authentication.SessionAuthentication', ], #'DEFAULT_PERMISSION_CLASSES': [ # 'fair_database.permissions.DataPermission', - #], + # ], } REST_AUTH = { - 'TOKEN_SERIALIZER': 'user_app.serializers.KnoxSerializer', - 'USER_DETAILS_SERIALIZER': 'dj_rest_auth.serializers.UserDetailsSerializer', - 'TOKEN_MODEL': 'knox.models.AuthToken', - 'TOKEN_CREATOR': 'user_app.util.create_knox_token', + "TOKEN_SERIALIZER": "user_app.serializers.KnoxSerializer", + "USER_DETAILS_SERIALIZER": "dj_rest_auth.serializers.UserDetailsSerializer", + "TOKEN_MODEL": "knox.models.AuthToken", + "TOKEN_CREATOR": "user_app.util.create_knox_token", } # allauth settings HEADLESS_ONLY = True -ACCOUNT_EMAIL_VERIFICATION = 'none' +ACCOUNT_EMAIL_VERIFICATION = "none" # to enable ORCID, register for credentials through ORCID and fill out client_id and secret SOCIALACCOUNT_PROVIDERS = { - 'orcid': { - 'APPS': [ + "orcid": { + "APPS": [ { - 'client_id': '', - 'secret': '', - 'key': '', + "client_id": "", + "secret": "", + "key": "", } - ], - 'SCOPE': [ - 'profile', 'email', + "SCOPE": [ + "profile", + "email", ], - 'AUTH_PARAMETERS': { - 'access_type': 'online' - }, + "AUTH_PARAMETERS": {"access_type": "online"}, # Base domain of the API. Default value: 'orcid.org', for the production API - 'BASE_DOMAIN':'sandbox.orcid.org', # for the sandbox API + "BASE_DOMAIN": "sandbox.orcid.org", # for the sandbox API # Member API or Public API? Default: False (for the public API) - 'MEMBER_API': False, + "MEMBER_API": False, } } @@ -140,9 +138,9 @@ # https://docs.djangoproject.com/en/5.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -152,16 +150,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -169,9 +167,9 @@ # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -182,14 +180,14 @@ # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_ROOT = os.path.join(BASE_DIR, 'static') -STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATIC_URL = "/static/" -#instead of doing this, create a create a new media_root +# instead of doing this, create a create a new media_root MEDIA_ROOT = os.path.join(BASE_DIR, "media") -MEDIA_URL = '/media/' +MEDIA_URL = "/media/" # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index c9c2e991..cf6632c4 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -4,161 +4,218 @@ from django.conf import settings from django.contrib.auth.models import User from rest_framework import status -from rest_framework.test import APIClient, APITestCase +from rest_framework.test import APITestCase from data.models import DataFile + def find(filename): - return os.path.join(os.path.dirname(__file__), "../../example_data/1d_data", filename) + return os.path.join( + os.path.dirname(__file__), "../../example_data/1d_data", filename + ) + def auth_header(response): - return {'Authorization': 'Token ' + response.data['token']} + return {"Authorization": "Token " + response.data["token"]} + class DataListPermissionsTests(APITestCase): - ''' Test permissions of data views using user_app for authentication. ''' + """Test permissions of data views using user_app for authentication.""" def setUp(self): - self.user = User.objects.create_user(username="testUser", password="secret", id=1, - email="email@domain.com") - self.user2 = User.objects.create_user(username="testUser2", password="secret", id=2, - email="email2@domain.com") - unowned_test_data = DataFile.objects.create(id=1, file_name="cyl_400_40.txt", - is_public=True) - unowned_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), 'rb')) - private_test_data = DataFile.objects.create(id=2, current_user=self.user, - file_name="cyl_400_20.txt", is_public=False) - private_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), 'rb')) - public_test_data = DataFile.objects.create(id=3, current_user=self.user, - file_name="cyl_testdata.txt", is_public=True) - public_test_data.file.save("cyl_testdata.txt", open(find("cyl_testdata.txt"), 'rb')) + self.user = User.objects.create_user( + username="testUser", password="secret", id=1, email="email@domain.com" + ) + self.user2 = User.objects.create_user( + username="testUser2", password="secret", id=2, email="email2@domain.com" + ) + unowned_test_data = DataFile.objects.create( + id=1, file_name="cyl_400_40.txt", is_public=True + ) + unowned_test_data.file.save( + "cyl_400_40.txt", open(find("cyl_400_40.txt"), "rb") + ) + private_test_data = DataFile.objects.create( + id=2, current_user=self.user, file_name="cyl_400_20.txt", is_public=False + ) + private_test_data.file.save( + "cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb") + ) + public_test_data = DataFile.objects.create( + id=3, current_user=self.user, file_name="cyl_testdata.txt", is_public=True + ) + public_test_data.file.save( + "cyl_testdata.txt", open(find("cyl_testdata.txt"), "rb") + ) self.login_data_1 = { - 'username': 'testUser', - 'password': 'secret', - 'email': 'email@domain.com' + "username": "testUser", + "password": "secret", + "email": "email@domain.com", } self.login_data_2 = { - 'username': 'testUser2', - 'password': 'secret', - 'email': 'email2@domain.com' + "username": "testUser2", + "password": "secret", + "email": "email2@domain.com", } # Authenticated user can view list of data # TODO: change to reflect inclusion of owned private data def test_list_authenticated(self): - token = self.client.post('/auth/login/', data=self.login_data_1) - response = self.client.get('/v1/data/list/', headers=auth_header(token)) - response2 = self.client.get('/v1/data/list/testUser/', headers=auth_header(token)) - self.assertEqual(response.data, - {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) - self.assertEqual(response2.data, - {"user_data_ids": {2: "cyl_400_20.txt", 3: "cyl_testdata.txt"}}) + token = self.client.post("/auth/login/", data=self.login_data_1) + response = self.client.get("/v1/data/list/", headers=auth_header(token)) + response2 = self.client.get( + "/v1/data/list/testUser/", headers=auth_header(token) + ) + self.assertEqual( + response.data, + {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}, + ) + self.assertEqual( + response2.data, + {"user_data_ids": {2: "cyl_400_20.txt", 3: "cyl_testdata.txt"}}, + ) # Authenticated user cannot view other users' private data on list # TODO: Change response codes def test_list_authenticated_2(self): - token = self.client.post('/auth/login/', data=self.login_data_2) - response = self.client.get('/v1/data/list/', headers=auth_header(token)) - response2 = self.client.get('/v1/data/list/testUser/', headers=auth_header(token)) - response3 = self.client.get('/v1/data/list/testUser2/', headers=auth_header(token)) - self.assertEqual(response.data, - {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) + token = self.client.post("/auth/login/", data=self.login_data_2) + response = self.client.get("/v1/data/list/", headers=auth_header(token)) + response2 = self.client.get( + "/v1/data/list/testUser/", headers=auth_header(token) + ) + response3 = self.client.get( + "/v1/data/list/testUser2/", headers=auth_header(token) + ) + self.assertEqual( + response.data, + {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}, + ) self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response3.data, {"user_data_ids": {}}) # Unauthenticated user can view list of public data def test_list_unauthenticated(self): - response = self.client.get('/v1/data/list/') - response2 = self.client.get('/v1/data/list/testUser/') - self.assertEqual(response.data, - {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}) + response = self.client.get("/v1/data/list/") + response2 = self.client.get("/v1/data/list/testUser/") + self.assertEqual( + response.data, + {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}, + ) self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) # Authenticated user can load public data and owned private data def test_load_authenticated(self): - token = self.client.post('/auth/login/', data=self.login_data_1) - response = self.client.get('/v1/data/load/1/', headers=auth_header(token)) - response2 = self.client.get('/v1/data/load/2/', headers=auth_header(token)) + token = self.client.post("/auth/login/", data=self.login_data_1) + response = self.client.get("/v1/data/load/1/", headers=auth_header(token)) + response2 = self.client.get("/v1/data/load/2/", headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Authenticated user cannot load others' private data def test_load_unauthorized(self): - token = self.client.post('/auth/login/', data=self.login_data_2) - response = self.client.get('/v1/data/load/2/', headers=auth_header(token)) - response2 = self.client.get('/v1/data/load/3/', headers=auth_header(token)) + token = self.client.post("/auth/login/", data=self.login_data_2) + response = self.client.get("/v1/data/load/2/", headers=auth_header(token)) + response2 = self.client.get("/v1/data/load/3/", headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Unauthenticated user can load public data only def test_load_unauthenticated(self): - response = self.client.get('/v1/data/load/1/') - response2 = self.client.get('/v1/data/load/2/') - response3 = self.client.get('/v1/data/load/3/') + response = self.client.get("/v1/data/load/1/") + response2 = self.client.get("/v1/data/load/2/") + response3 = self.client.get("/v1/data/load/3/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response3.status_code, status.HTTP_200_OK) # Authenticated user can upload data def test_upload_authenticated(self): - token = self.client.post('/auth/login/', data=self.login_data_1) - file = open(find('cyl_testdata1.txt'), 'rb') - data = { - 'file': file, - 'is_public': False - } - response = self.client.post('/v1/data/upload/', data=data, headers=auth_header(token)) + token = self.client.post("/auth/login/", data=self.login_data_1) + file = open(find("cyl_testdata1.txt"), "rb") + data = {"file": file, "is_public": False} + response = self.client.post( + "/v1/data/upload/", data=data, headers=auth_header(token) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {"current_user": 'testUser', "authenticated": True, - "file_id": 4, "file_alternative_name": "cyl_testdata1.txt", "is_public": False}) + self.assertEqual( + response.data, + { + "current_user": "testUser", + "authenticated": True, + "file_id": 4, + "file_alternative_name": "cyl_testdata1.txt", + "is_public": False, + }, + ) DataFile.objects.get(id=4).delete() # Unauthenticated user can upload public data only def test_upload_unauthenticated(self): - file = open(find('cyl_testdata2.txt'), 'rb') - file2 = open(find('cyl_testdata2.txt'), 'rb') - data = { - 'file': file, - 'is_public': True - } - data2 = { - 'file': file2, - 'is_public': False - } - response = self.client.post('/v1/data/upload/', data=data) - response2 = self.client.post('/v1/data/upload/', data=data2) + file = open(find("cyl_testdata2.txt"), "rb") + file2 = open(find("cyl_testdata2.txt"), "rb") + data = {"file": file, "is_public": True} + data2 = {"file": file2, "is_public": False} + response = self.client.post("/v1/data/upload/", data=data) + response2 = self.client.post("/v1/data/upload/", data=data2) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {"current_user": '', "authenticated": False, - "file_id": 4, "file_alternative_name": "cyl_testdata2.txt", - "is_public": True}) + self.assertEqual( + response.data, + { + "current_user": "", + "authenticated": False, + "file_id": 4, + "file_alternative_name": "cyl_testdata2.txt", + "is_public": True, + }, + ) self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) # Authenticated user can update own data def test_upload_put_authenticated(self): - token = self.client.post('/auth/login/', data=self.login_data_1) - data = { - "is_public": False - } - response = self.client.put('/v1/data/upload/2/', data=data, headers=auth_header(token)) - response2 = self.client.put('/v1/data/upload/3/', data=data, headers=auth_header(token)) - self.assertEqual(response.data, - {"current_user": 'testUser', "authenticated": True, "file_id": 2, - "file_alternative_name": "cyl_400_20.txt", "is_public": False}) - self.assertEqual(response2.data, - {"current_user": 'testUser', "authenticated": True, "file_id": 3, - "file_alternative_name": "cyl_testdata.txt", "is_public": False}) + token = self.client.post("/auth/login/", data=self.login_data_1) + data = {"is_public": False} + response = self.client.put( + "/v1/data/upload/2/", data=data, headers=auth_header(token) + ) + response2 = self.client.put( + "/v1/data/upload/3/", data=data, headers=auth_header(token) + ) + self.assertEqual( + response.data, + { + "current_user": "testUser", + "authenticated": True, + "file_id": 2, + "file_alternative_name": "cyl_400_20.txt", + "is_public": False, + }, + ) + self.assertEqual( + response2.data, + { + "current_user": "testUser", + "authenticated": True, + "file_id": 3, + "file_alternative_name": "cyl_testdata.txt", + "is_public": False, + }, + ) DataFile.objects.get(id=3).is_public = True # Authenticated user cannot update unowned data def test_upload_put_unauthorized(self): - token = self.client.post('/auth/login/', data=self.login_data_2) + token = self.client.post("/auth/login/", data=self.login_data_2) file = open(find("cyl_400_40.txt")) - data = { - "file": file, - "is_public": False - } - response = self.client.put('/v1/data/upload/1/', data=data, headers=auth_header(token)) - response2 = self.client.put('/v1/data/upload/2/', data=data, headers=auth_header(token)) - response3 = self.client.put('/v1/data/upload/3/', data=data, headers=auth_header(token)) + data = {"file": file, "is_public": False} + response = self.client.put( + "/v1/data/upload/1/", data=data, headers=auth_header(token) + ) + response2 = self.client.put( + "/v1/data/upload/2/", data=data, headers=auth_header(token) + ) + response3 = self.client.put( + "/v1/data/upload/3/", data=data, headers=auth_header(token) + ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response2.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response3.status_code, status.HTTP_404_NOT_FOUND) @@ -166,43 +223,40 @@ def test_upload_put_unauthorized(self): # Unauthenticated user cannot update data def test_upload_put_unauthenticated(self): file = open(find("cyl_400_40.txt")) - data = { - "file": file, - "is_public": False - } - response = self.client.put('/v1/data/upload/1/', data=data) - response2 = self.client.put('/v1/data/upload/2/', data=data) - response3 = self.client.put('/v1/data/upload/3/', data=data) + data = {"file": file, "is_public": False} + response = self.client.put("/v1/data/upload/1/", data=data) + response2 = self.client.put("/v1/data/upload/2/", data=data) + response3 = self.client.put("/v1/data/upload/3/", data=data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response3.status_code, status.HTTP_403_FORBIDDEN) # Authenticated user can download public and own data def test_download_authenticated(self): - token = self.client.post('/auth/login/', data=self.login_data_1) - response = self.client.get('/v1/data/1/download/', headers=auth_header(token)) - response2 = self.client.get('/v1/data/2/download/', headers=auth_header(token)) - response3 = self.client.get('/v1/data/3/download/', headers=auth_header(token)) + token = self.client.post("/auth/login/", data=self.login_data_1) + response = self.client.get("/v1/data/1/download/", headers=auth_header(token)) + response2 = self.client.get("/v1/data/2/download/", headers=auth_header(token)) + response3 = self.client.get("/v1/data/3/download/", headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) self.assertEqual(response3.status_code, status.HTTP_200_OK) # Authenticated user cannot download others' data def test_download_unauthorized(self): - token = self.client.post('/auth/login/', data=self.login_data_2) - response = self.client.get('/v1/data/2/download/', headers=auth_header(token)) - response2 = self.client.get('/v1/data/3/download/', headers=auth_header(token)) + token = self.client.post("/auth/login/", data=self.login_data_2) + response = self.client.get("/v1/data/2/download/", headers=auth_header(token)) + response2 = self.client.get("/v1/data/3/download/", headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Unauthenticated user cannot download private data def test_download_unauthenticated(self): - response = self.client.get('/v1/data/1/download/') - response2 = self.client.get('/v1/data/2/download/') - response3 = self.client.get('/v1/data/3/download/') + response = self.client.get("/v1/data/1/download/") + response2 = self.client.get("/v1/data/2/download/") + response3 = self.client.get("/v1/data/3/download/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response3.status_code, status.HTTP_200_OK) def tearDown(self): - shutil.rmtree(settings.MEDIA_ROOT) \ No newline at end of file + shutil.rmtree(settings.MEDIA_ROOT) diff --git a/sasdata/fair_database/fair_database/upload_example_data.py b/sasdata/fair_database/fair_database/upload_example_data.py index bf014cf2..1b16fdca 100644 --- a/sasdata/fair_database/fair_database/upload_example_data.py +++ b/sasdata/fair_database/fair_database/upload_example_data.py @@ -4,7 +4,8 @@ from glob import glob -EXAMPLE_DATA_DIR = os.environ.get("EXAMPLE_DATA_DIR", '../../example_data') +EXAMPLE_DATA_DIR = os.environ.get("EXAMPLE_DATA_DIR", "../../example_data") + def parse_1D(): dir_1d = os.path.join(EXAMPLE_DATA_DIR, "1d_data") @@ -14,6 +15,7 @@ def parse_1D(): for file_path in glob(os.path.join(dir_1d, "*")): upload_file(file_path) + def parse_2D(): dir_2d = os.path.join(EXAMPLE_DATA_DIR, "2d_data") if not os.path.isdir(dir_2d): @@ -22,6 +24,7 @@ def parse_2D(): for file_path in glob(os.path.join(dir_2d, "*")): upload_file(file_path) + def parse_sesans(): sesans_dir = os.path.join(EXAMPLE_DATA_DIR, "sesans_data") if not os.path.isdir(sesans_dir): @@ -30,12 +33,14 @@ def parse_sesans(): for file_path in glob(os.path.join(sesans_dir, "*")): upload_file(file_path) + def upload_file(file_path): - url = 'http://localhost:8000/v1/data/upload/' - file = open(file_path, 'rb') - requests.request('POST', url, data={'is_public': True}, files={'file':file}) + url = "http://localhost:8000/v1/data/upload/" + file = open(file_path, "rb") + requests.request("POST", url, data={"is_public": True}, files={"file": file}) + -if __name__ == '__main__': +if __name__ == "__main__": parse_1D() parse_2D() parse_sesans() diff --git a/sasdata/fair_database/fair_database/urls.py b/sasdata/fair_database/fair_database/urls.py index 89bac77c..0cfeb0ac 100644 --- a/sasdata/fair_database/fair_database/urls.py +++ b/sasdata/fair_database/fair_database/urls.py @@ -14,12 +14,13 @@ 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.urls import include, path, re_path urlpatterns = [ re_path(r"^(?P<version>(v1))/data/", include("data.urls")), path("admin/", admin.site.urls), - path("accounts/", include("allauth.urls")), #needed for social auth - path('auth/', include('user_app.urls')), + path("accounts/", include("allauth.urls")), # needed for social auth + path("auth/", include("user_app.urls")), ] diff --git a/sasdata/fair_database/fair_database/wsgi.py b/sasdata/fair_database/fair_database/wsgi.py index cb087086..5dfc4819 100644 --- a/sasdata/fair_database/fair_database/wsgi.py +++ b/sasdata/fair_database/fair_database/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fair_database.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fair_database.settings") application = get_wsgi_application() diff --git a/sasdata/fair_database/manage.py b/sasdata/fair_database/manage.py index c74d5f9c..7d7e9724 100755 --- a/sasdata/fair_database/manage.py +++ b/sasdata/fair_database/manage.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fair_database.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fair_database.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +19,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/sasdata/fair_database/user_app/admin.py b/sasdata/fair_database/user_app/admin.py index 8c38f3f3..846f6b40 100644 --- a/sasdata/fair_database/user_app/admin.py +++ b/sasdata/fair_database/user_app/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/sasdata/fair_database/user_app/apps.py b/sasdata/fair_database/user_app/apps.py index f2d1d417..83a29dec 100644 --- a/sasdata/fair_database/user_app/apps.py +++ b/sasdata/fair_database/user_app/apps.py @@ -2,5 +2,5 @@ class UserAppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'user_app' + default_auto_field = "django.db.models.BigAutoField" + name = "user_app" diff --git a/sasdata/fair_database/user_app/models.py b/sasdata/fair_database/user_app/models.py index 71a83623..6b202199 100644 --- a/sasdata/fair_database/user_app/models.py +++ b/sasdata/fair_database/user_app/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/sasdata/fair_database/user_app/serializers.py b/sasdata/fair_database/user_app/serializers.py index 7d315377..c739fea2 100644 --- a/sasdata/fair_database/user_app/serializers.py +++ b/sasdata/fair_database/user_app/serializers.py @@ -7,8 +7,9 @@ class KnoxSerializer(serializers.Serializer): """ Serializer for Knox authentication. """ + token = serializers.SerializerMethodField() user = UserDetailsSerializer() def get_token(self, obj): - return obj["token"][1] \ No newline at end of file + return obj["token"][1] diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index b3f203a3..664fd963 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -4,148 +4,166 @@ from django.contrib.auth.models import User + # Create your tests here. class AuthTests(TestCase): - def setUp(self): self.client = APIClient() self.register_data = { "email": "email@domain.org", "username": "testUser", "password1": "sasview!", - "password2": "sasview!" + "password2": "sasview!", } self.login_data = { "username": "testUser", "email": "email@domain.org", - "password": "sasview!" + "password": "sasview!", } def auth_header(self, response): - return {'Authorization': 'Token ' + response.data['token']} + return {"Authorization": "Token " + response.data["token"]} # Test if registration successfully creates a new user and logs in def test_register(self): - response = self.client.post('/auth/register/',data=self.register_data) + response = self.client.post("/auth/register/", data=self.register_data) user = User.objects.get(username="testUser") - response2 = self.client.get('/auth/user/', headers=self.auth_header(response)) + response2 = self.client.get("/auth/user/", headers=self.auth_header(response)) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(user.email, self.register_data["email"]) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Test if login successful def test_login(self): - User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") - response = self.client.post('/auth/login/', data=self.login_data) - response2 = self.client.get('/auth/user/', headers=self.auth_header(response)) + User.objects.create_user( + username="testUser", password="sasview!", email="email@domain.org" + ) + response = self.client.post("/auth/login/", data=self.login_data) + response2 = self.client.get("/auth/user/", headers=self.auth_header(response)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Test simultaneous login by multiple clients def test_multiple_login(self): - User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + User.objects.create_user( + username="testUser", password="sasview!", email="email@domain.org" + ) client2 = APIClient() - response = self.client.post('/auth/login/', data=self.login_data) - response2 = client2.post('/auth/login/', data=self.login_data) + response = self.client.post("/auth/login/", data=self.login_data) + response2 = client2.post("/auth/login/", data=self.login_data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) self.assertNotEqual(response.content, response2.content) # Test get user information def test_user_get(self): - user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + user = User.objects.create_user( + username="testUser", password="sasview!", email="email@domain.org" + ) self.client.force_authenticate(user=user) - response = self.client.get('/auth/user/') + response = self.client.get("/auth/user/") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, - b'{"pk":1,"username":"testUser","email":"email@domain.org","first_name":"","last_name":""}') + self.assertEqual( + response.content, + b'{"pk":1,"username":"testUser","email":"email@domain.org","first_name":"","last_name":""}', + ) # Test changing username def test_user_put_username(self): - user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + user = User.objects.create_user( + username="testUser", password="sasview!", email="email@domain.org" + ) self.client.force_authenticate(user=user) - data = { - "username": "newName" - } - response = self.client.put('/auth/user/', data=data) + data = {"username": "newName"} + response = self.client.put("/auth/user/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, - b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}') + self.assertEqual( + response.content, + b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}', + ) # Test changing username and first and last name def test_user_put_name(self): - user = User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + user = User.objects.create_user( + username="testUser", password="sasview!", email="email@domain.org" + ) self.client.force_authenticate(user=user) - data = { - "username": "newName", - "first_name": "Clark", - "last_name": "Kent" - } - response = self.client.put('/auth/user/', data=data) + data = {"username": "newName", "first_name": "Clark", "last_name": "Kent"} + response = self.client.put("/auth/user/", data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, - b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}') + self.assertEqual( + response.content, + b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}', + ) # Test user info inaccessible when unauthenticated def test_user_unauthenticated(self): - response = self.client.get('/auth/user/') + response = self.client.get("/auth/user/") self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertEqual(response.content, - b'{"detail":"Authentication credentials were not provided."}') + self.assertEqual( + response.content, + b'{"detail":"Authentication credentials were not provided."}', + ) # Test logout is successful after login def test_login_logout(self): - User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") - self.client.post('/auth/login/', data=self.login_data) - response = self.client.post('/auth/logout/') - response2 = self.client.get('/auth/user/') + User.objects.create_user( + username="testUser", password="sasview!", email="email@domain.org" + ) + self.client.post("/auth/login/", data=self.login_data) + response = self.client.post("/auth/logout/") + response2 = self.client.get("/auth/user/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEqual(response2.status_code, status.HTTP_401_UNAUTHORIZED) # Test logout is successful after registration def test_register_logout(self): - self.client.post('/auth/register/', data=self.register_data) - response = self.client.post('/auth/logout/') - response2 = self.client.get('/auth/user/') + self.client.post("/auth/register/", data=self.register_data) + response = self.client.post("/auth/logout/") + response2 = self.client.get("/auth/user/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEqual(response2.status_code, status.HTTP_401_UNAUTHORIZED) def test_multiple_logout(self): - User.objects.create_user(username="testUser", password="sasview!", email="email@domain.org") + User.objects.create_user( + username="testUser", password="sasview!", email="email@domain.org" + ) client2 = APIClient() - self.client.post('/auth/login/', data=self.login_data) - token = client2.post('/auth/login/', data=self.login_data) - response = self.client.post('/auth/logout/') - response2 = client2.get('/auth/user/', headers=self.auth_header(token)) - response3 = client2.post('/auth/logout/') + self.client.post("/auth/login/", data=self.login_data) + token = client2.post("/auth/login/", data=self.login_data) + response = self.client.post("/auth/logout/") + response2 = client2.get("/auth/user/", headers=self.auth_header(token)) + response3 = client2.post("/auth/logout/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) self.assertEqual(response3.status_code, status.HTTP_200_OK) # Test login is successful after registering then logging out def test_register_login(self): - register_response = self.client.post('/auth/register/', data=self.register_data) - logout_response = self.client.post('/auth/logout/') - login_response = self.client.post('/auth/login/', data=self.login_data) + register_response = self.client.post("/auth/register/", data=self.register_data) + logout_response = self.client.post("/auth/logout/") + login_response = self.client.post("/auth/login/", data=self.login_data) self.assertEqual(register_response.status_code, status.HTTP_201_CREATED) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) # Test password is successfully changed def test_password_change(self): - token = self.client.post('/auth/register/', data=self.register_data) + token = self.client.post("/auth/register/", data=self.register_data) data = { "new_password1": "sasview?", "new_password2": "sasview?", - "old_password": "sasview!" + "old_password": "sasview!", } l_data = self.login_data l_data["password"] = "sasview?" - response = self.client.post('/auth/password/change/', data=data, headers=self.auth_header(token)) - logout_response = self.client.post('/auth/logout/') - login_response = self.client.post('/auth/login/', data=l_data) + response = self.client.post( + "/auth/password/change/", data=data, headers=self.auth_header(token) + ) + logout_response = self.client.post("/auth/logout/") + login_response = self.client.post("/auth/login/", data=l_data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) @@ -161,4 +179,3 @@ def test_password_change(self): # unauthenticated user cannot modify data # logged-in user cannot modify data other than their own # logged-in user cannot access the private data of others - diff --git a/sasdata/fair_database/user_app/urls.py b/sasdata/fair_database/user_app/urls.py index 339d7d8b..808cbfce 100644 --- a/sasdata/fair_database/user_app/urls.py +++ b/sasdata/fair_database/user_app/urls.py @@ -1,15 +1,14 @@ from django.urls import path -from dj_rest_auth.views import (LogoutView, - UserDetailsView, PasswordChangeView) +from dj_rest_auth.views import LogoutView, UserDetailsView, PasswordChangeView from .views import KnoxLoginView, KnoxRegisterView, OrcidLoginView -'''Urls for authentication. Orcid login not functional.''' +"""Urls for authentication. Orcid login not functional.""" urlpatterns = [ - path('register/', KnoxRegisterView.as_view(), name='register'), - path('login/', KnoxLoginView.as_view(), name='login'), - path('logout/', LogoutView.as_view(), name='logout'), - path('user/', UserDetailsView.as_view(), name='view user information'), - path('password/change/', PasswordChangeView.as_view(), name='change password'), - path('login/orcid/', OrcidLoginView.as_view(), name='orcid login') -] \ No newline at end of file + path("register/", KnoxRegisterView.as_view(), name="register"), + path("login/", KnoxLoginView.as_view(), name="login"), + path("logout/", LogoutView.as_view(), name="logout"), + path("user/", UserDetailsView.as_view(), name="view user information"), + path("password/change/", PasswordChangeView.as_view(), name="change password"), + path("login/orcid/", OrcidLoginView.as_view(), name="orcid login"), +] diff --git a/sasdata/fair_database/user_app/util.py b/sasdata/fair_database/user_app/util.py index ab9bcd0d..c6b43cc6 100644 --- a/sasdata/fair_database/user_app/util.py +++ b/sasdata/fair_database/user_app/util.py @@ -3,4 +3,4 @@ def create_knox_token(token_model, user, serializer): token = AuthToken.objects.create(user=user) - return token \ No newline at end of file + return token diff --git a/sasdata/fair_database/user_app/views.py b/sasdata/fair_database/user_app/views.py index 4a55fdc7..32113a74 100644 --- a/sasdata/fair_database/user_app/views.py +++ b/sasdata/fair_database/user_app/views.py @@ -1,5 +1,3 @@ -from django.conf import settings - from rest_framework.response import Response from dj_rest_auth.views import LoginView from dj_rest_auth.registration.views import RegisterView, SocialLoginView @@ -10,33 +8,33 @@ from user_app.serializers import KnoxSerializer from user_app.util import create_knox_token -#Login using knox tokens rather than django-rest-framework tokens. +# Login using knox tokens rather than django-rest-framework tokens. -class KnoxLoginView(LoginView): +class KnoxLoginView(LoginView): def get_response(self): serializer_class = self.get_response_serializer() - data = { - 'user': self.user, - 'token': self.token - } - serializer = serializer_class(instance=data, context={'request': self.request}) + data = {"user": self.user, "token": self.token} + serializer = serializer_class(instance=data, context={"request": self.request}) return Response(serializer.data, status=200) + # Registration using knox tokens rather than django-rest-framework tokens. class KnoxRegisterView(RegisterView): - def get_response_data(self, user): - return KnoxSerializer({'user': user, 'token': self.token}).data + return KnoxSerializer({"user": user, "token": self.token}).data def perform_create(self, serializer): user = serializer.save(self.request) - self.token = create_knox_token(None,user,None) - complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, None) + self.token = create_knox_token(None, user, None) + complete_signup( + self.request._request, user, allauth_settings.EMAIL_VERIFICATION, None + ) return user + # For ORCID login class OrcidLoginView(SocialLoginView): - adapter_class = OrcidOAuth2Adapter \ No newline at end of file + adapter_class = OrcidOAuth2Adapter From d3b6457f6983eb9e1ada5f75c7ca29ca4dc554d5 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 15:13:34 -0500 Subject: [PATCH 069/129] Fix permissions bug that prevented access to a user's own private data --- sasdata/fair_database/fair_database/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sasdata/fair_database/fair_database/permissions.py b/sasdata/fair_database/fair_database/permissions.py index fb194938..e62e7257 100644 --- a/sasdata/fair_database/fair_database/permissions.py +++ b/sasdata/fair_database/fair_database/permissions.py @@ -2,7 +2,7 @@ def is_owner(request, obj): - return request.user.is_authenticated and request.user.id == obj.current_user + return request.user.is_authenticated and request.user == obj.current_user class DataPermission(BasePermission): From 82818f314d23ed124cd62806d68336ed3a08c487 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 15:26:10 -0500 Subject: [PATCH 070/129] Change permissions handling for load and upload --- sasdata/fair_database/data/views.py | 49 ++++++++----------- .../fair_database/test_permissions.py | 12 +++-- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 1a3f0d2c..789220d8 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -46,19 +46,13 @@ def data_info(request, db_id, version=None): if request.method == "GET": loader = Loader() data_db = get_object_or_404(DataFile, id=db_id) - if data_db.is_public: - data_list = loader.load(data_db.file.path) - contents = [str(data) for data in data_list] - return_data = {data_db.file_name: contents} - # rewrite with "user.is_authenticated" - elif (data_db.current_user == request.user) and request.user.is_authenticated: - data_list = loader.load(data_db.file.path) - contents = [str(data) for data in data_list] - return_data = {data_db.file_name: contents} - else: - return HttpResponseBadRequest( - "Database is either not public or wrong auth token" + if not permissions.check_permissions(request, data_db): + return HttpResponseForbidden( + "Data is either not public or wrong auth token" ) + data_list = loader.load(data_db.file.path) + contents = [str(data) for data in data_list] + return_data = {data_db.file_name: contents} return Response(return_data) return HttpResponseBadRequest() @@ -93,22 +87,21 @@ def upload(request, data_id=None, version=None): # updates file elif request.method == "PUT": - if request.user.is_authenticated: - db = get_object_or_404(DataFile, current_user=request.user.id, id=data_id) - form = DataFileForm(request.data, request.FILES, instance=db) - if form.is_valid(): - form.save() - serializer = DataFileSerializer( - db, - data={ - "file_name": os.path.basename(form.instance.file.path), - "current_user": request.user.id, - }, - context={"is_public": db.is_public}, - partial=True, - ) - else: - return HttpResponseForbidden("user is not logged in") + db = get_object_or_404(DataFile, id=data_id) + if not permissions.check_permissions(request, db): + return HttpResponseForbidden("must be the data owner to modify") + form = DataFileForm(request.data, request.FILES, instance=db) + if form.is_valid(): + form.save() + serializer = DataFileSerializer( + db, + data={ + "file_name": os.path.basename(form.instance.file.path), + "current_user": request.user.id, + }, + context={"is_public": db.is_public}, + partial=True, + ) if serializer.is_valid(raise_exception=True): serializer.save() diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index cf6632c4..f7e32a5c 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -108,15 +108,17 @@ def test_load_authenticated(self): token = self.client.post("/auth/login/", data=self.login_data_1) response = self.client.get("/v1/data/load/1/", headers=auth_header(token)) response2 = self.client.get("/v1/data/load/2/", headers=auth_header(token)) + response3 = self.client.get("/v1/data/load/3/", headers=auth_header(token)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) + self.assertEqual(response3.status_code, status.HTTP_200_OK) # Authenticated user cannot load others' private data def test_load_unauthorized(self): token = self.client.post("/auth/login/", data=self.login_data_2) response = self.client.get("/v1/data/load/2/", headers=auth_header(token)) response2 = self.client.get("/v1/data/load/3/", headers=auth_header(token)) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Unauthenticated user can load public data only @@ -125,7 +127,7 @@ def test_load_unauthenticated(self): response2 = self.client.get("/v1/data/load/2/") response3 = self.client.get("/v1/data/load/3/") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response3.status_code, status.HTTP_200_OK) # Authenticated user can upload data @@ -216,9 +218,9 @@ def test_upload_put_unauthorized(self): response3 = self.client.put( "/v1/data/upload/3/", data=data, headers=auth_header(token) ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response2.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response3.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response3.status_code, status.HTTP_403_FORBIDDEN) # Unauthenticated user cannot update data def test_upload_put_unauthenticated(self): From 8c8cdf7ce9e2856ce3dea28d036b06e7a98a16af Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 15:30:20 -0500 Subject: [PATCH 071/129] Change permissions handling for download --- sasdata/fair_database/data/views.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 789220d8..a17bca64 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -121,12 +121,8 @@ def upload(request, data_id=None, version=None): def download(request, data_id, version=None): if request.method == "GET": data = get_object_or_404(DataFile, id=data_id) - if not data.is_public: - # add session key later - if not request.user.is_authenticated: - return HttpResponseForbidden("data is private, must log in") - if not request.user == data.current_user: - return HttpResponseForbidden("data is private") + if not permissions.check_permissions(request, data): + return HttpResponseForbidden("data is private") # TODO add issues later try: file = open(data.file.path, "rb") From 7a59e5a321dba82efe518a3b78fd524080457764 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 15:32:28 -0500 Subject: [PATCH 072/129] Return bad request for non put or push upload --- sasdata/fair_database/data/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index a17bca64..09088589 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -102,6 +102,8 @@ def upload(request, data_id=None, version=None): context={"is_public": db.is_public}, partial=True, ) + else: + return HttpResponseBadRequest() if serializer.is_valid(raise_exception=True): serializer.save() From 5a46da1fba65f6fab65b74507b7f22d09da29f5e Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 16:26:07 -0500 Subject: [PATCH 073/129] Add tests for accessing nonexistent data --- sasdata/fair_database/data/tests.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 28243173..330c7ff4 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -44,6 +44,11 @@ def test_does_list_user(self): request = self.client.get("/v1/data/list/testUser/", user=self.user) self.assertEqual(request.data, {"user_data_ids": {3: "cyl_400_20.txt"}}) + # Test list a nonexistent user's data + def test_list_other_user(self): + request = self.client.get("/v1/data/list/fakeUser/") + self.assertEqual(request.status_code, status.HTTP_400_BAD_REQUEST) + # Test loading a public data file def test_does_load_data_info_public(self): request = self.client.get("/v1/data/load/1/") @@ -54,6 +59,11 @@ def test_does_load_data_info_private(self): request = self.client.get("/v1/data/load/3/") self.assertEqual(request.status_code, status.HTTP_200_OK) + # Test loading data that does not exist + def test_load_data_info_nonexistent(self): + request = self.client.get("/v1/data/load/5/") + self.assertEqual(request.status_code, status.HTTP_404_NOT_FOUND) + def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) @@ -124,6 +134,7 @@ def test_does_file_upload_update(self): ) DataFile.objects.get(id=2).delete() + # Test updating a public file def test_public_file_upload_update(self): data_object = DataFile.objects.create( id=3, current_user=self.user, file_name="cyl_testdata.txt", is_public=True @@ -152,6 +163,13 @@ def test_unauthorized_file_upload_update(self): self.assertEqual(request.status_code, status.HTTP_403_FORBIDDEN) DataFile.objects.get(id=2).delete() + # Test update nonexistent file fails + def test_file_upload_update_not_found(self): + file = open(find("cyl_400_40.txt")) + data = {"file": file, "is_public": False} + request = self.client2.put("/v1/data/upload/5/", data=data) + self.assertEqual(request.status_code, status.HTTP_404_NOT_FOUND) + # Test file download def test_does_download(self): request = self.client.get("/v1/data/2/download/") @@ -165,5 +183,10 @@ def test_unauthorized_download(self): request2 = self.client2.get("/v1/data/2/download/") self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) + # Test download nonexistent file + def test_download_nonexistent(self): + request = self.client.get("/v1/data/5/download/") + self.assertEqual(request.status_code, status.HTTP_404_NOT_FOUND) + def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) From 6fdf0ec91262811b0514aa7e7a5e7fcd79907274 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 11 Feb 2025 16:51:48 -0500 Subject: [PATCH 074/129] Comment out unfinished test line --- sasdata/quantities/test_numerical_encoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sasdata/quantities/test_numerical_encoding.py b/sasdata/quantities/test_numerical_encoding.py index 80cfbad9..93642a3f 100644 --- a/sasdata/quantities/test_numerical_encoding.py +++ b/sasdata/quantities/test_numerical_encoding.py @@ -63,6 +63,6 @@ def test_numpy_dtypes_encode_decode(dtype): ]) def test_coo_matrix_encode_decode(shape, n, m, dtype): - i_indices = + #i_indices = values = np.arange(10) \ No newline at end of file From c85886d419857b99598efe12aae31428719bad47 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 12 Feb 2025 10:45:38 -0500 Subject: [PATCH 075/129] Read download files to ensure they close - attempt to fix unit test failures on windows --- sasdata/fair_database/fair_database/test_permissions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index f7e32a5c..1ed5edd2 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -239,6 +239,9 @@ def test_download_authenticated(self): response = self.client.get("/v1/data/1/download/", headers=auth_header(token)) response2 = self.client.get("/v1/data/2/download/", headers=auth_header(token)) response3 = self.client.get("/v1/data/3/download/", headers=auth_header(token)) + b"".join(response.streaming_content) + b"".join(response2.streaming_content) + b"".join(response3.streaming_content) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) self.assertEqual(response3.status_code, status.HTTP_200_OK) @@ -248,6 +251,7 @@ def test_download_unauthorized(self): token = self.client.post("/auth/login/", data=self.login_data_2) response = self.client.get("/v1/data/2/download/", headers=auth_header(token)) response2 = self.client.get("/v1/data/3/download/", headers=auth_header(token)) + b"".join(response2.streaming_content) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response2.status_code, status.HTTP_200_OK) @@ -256,6 +260,8 @@ def test_download_unauthenticated(self): response = self.client.get("/v1/data/1/download/") response2 = self.client.get("/v1/data/2/download/") response3 = self.client.get("/v1/data/3/download/") + b"".join(response.streaming_content) + b"".join(response2.streaming_content) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response3.status_code, status.HTTP_200_OK) From 90c9ee64d7761abd4b37e4350050474d310fc6c9 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 12 Feb 2025 10:45:38 -0500 Subject: [PATCH 076/129] Fix typo --- sasdata/fair_database/fair_database/test_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index 1ed5edd2..f13c6745 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -261,7 +261,7 @@ def test_download_unauthenticated(self): response2 = self.client.get("/v1/data/2/download/") response3 = self.client.get("/v1/data/3/download/") b"".join(response.streaming_content) - b"".join(response2.streaming_content) + b"".join(response3.streaming_content) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response3.status_code, status.HTTP_200_OK) From 7e378b4dfbaf1d849547dfeea18723bf9a376f11 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 13 Feb 2025 11:47:16 -0500 Subject: [PATCH 077/129] Preliminary documentation for models --- sasdata/fair_database/data/models.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 013ef0a9..cfffd940 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -3,8 +3,9 @@ from django.core.files.storage import FileSystemStorage -# Create your models here. class DataFile(models.Model): + """Database model for file contents.""" + # username current_user = models.ForeignKey( User, blank=True, null=True, on_delete=models.CASCADE @@ -29,3 +30,12 @@ class DataFile(models.Model): is_public = models.BooleanField( default=False, help_text="opt in to submit your data into example pool" ) + + +"""Database model for a set of data and associated metadata.""" + +"""Database model for group of DataSets associated by a varying parameter.""" + +"""Database model for tree of operations performed on a DataSet.""" + +"""Database model for a project save state.""" From a712765d03e70c423e022d63b0d2919d98a1dced Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 13 Feb 2025 11:58:15 -0500 Subject: [PATCH 078/129] Base abstract model for user and is_public --- .../0003_alter_datafile_is_public.py | 19 +++++++++++++++++ sasdata/fair_database/data/models.py | 21 ++++++++++++------- 2 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 sasdata/fair_database/data/migrations/0003_alter_datafile_is_public.py diff --git a/sasdata/fair_database/data/migrations/0003_alter_datafile_is_public.py b/sasdata/fair_database/data/migrations/0003_alter_datafile_is_public.py new file mode 100644 index 00000000..e6415eb8 --- /dev/null +++ b/sasdata/fair_database/data/migrations/0003_alter_datafile_is_public.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.5 on 2025-02-13 16:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("data", "0002_rename_data_datafile"), + ] + + operations = [ + migrations.AlterField( + model_name="datafile", + name="is_public", + field=models.BooleanField( + default=False, help_text="opt in to make your data public" + ), + ), + ] diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index cfffd940..7a563108 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -3,14 +3,26 @@ from django.core.files.storage import FileSystemStorage -class DataFile(models.Model): - """Database model for file contents.""" +class Data(models.Model): + """Base model for data.""" # username current_user = models.ForeignKey( User, blank=True, null=True, on_delete=models.CASCADE ) + # is the data public? + is_public = models.BooleanField( + default=False, help_text="opt in to make your data public" + ) + + class Meta: + abstract = True + + +class DataFile(Data): + """Database model for file contents.""" + # file name file_name = models.CharField( max_length=200, default=None, blank=True, null=True, help_text="File name" @@ -26,11 +38,6 @@ class DataFile(models.Model): storage=FileSystemStorage(), ) - # is the data public? - is_public = models.BooleanField( - default=False, help_text="opt in to submit your data into example pool" - ) - """Database model for a set of data and associated metadata.""" From 2a152fb876032680618adc58f37fc64fae7ec9cd Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 14 Feb 2025 14:24:37 -0500 Subject: [PATCH 079/129] Enable list other users' public data --- sasdata/fair_database/data/views.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 09088589..be9ce975 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -22,14 +22,10 @@ def list_data(request, username=None, version=None): if request.method == "GET": if username: data_list = {"user_data_ids": {}} - if username == request.user.username and request.user.is_authenticated: - private_data = DataFile.objects.filter(current_user=request.user.id) - for x in private_data: + private_data = DataFile.objects.filter(current_user=request.user.id) + for x in private_data: + if permissions.check_permissions(request, x): data_list["user_data_ids"][x.id] = x.file_name - else: - return HttpResponseBadRequest( - "user is not logged in, or username is not same as current user" - ) else: public_data = DataFile.objects.filter(is_public=True) data_list = {"public_data_ids": {}} From 8c7171f67c0dc9df9b74d2f53ac26a9ef0e87530 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 14 Feb 2025 14:27:15 -0500 Subject: [PATCH 080/129] 404 for list by nonexistent username --- sasdata/fair_database/data/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index be9ce975..47262377 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -1,5 +1,6 @@ import os +from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 from django.http import ( HttpResponseBadRequest, @@ -21,6 +22,7 @@ def list_data(request, username=None, version=None): if request.method == "GET": if username: + get_object_or_404(User, username=username) data_list = {"user_data_ids": {}} private_data = DataFile.objects.filter(current_user=request.user.id) for x in private_data: From bb7a944aab60a62520b8f570c9e311a5388ab904 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 14 Feb 2025 14:37:05 -0500 Subject: [PATCH 081/129] Switch to filter by username param not request user --- sasdata/fair_database/data/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 47262377..fcc782c5 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -22,9 +22,9 @@ def list_data(request, username=None, version=None): if request.method == "GET": if username: - get_object_or_404(User, username=username) + search_user = get_object_or_404(User, username=username) data_list = {"user_data_ids": {}} - private_data = DataFile.objects.filter(current_user=request.user.id) + private_data = DataFile.objects.filter(current_user=search_user) for x in private_data: if permissions.check_permissions(request, x): data_list["user_data_ids"][x.id] = x.file_name From 605647397f5ae40b01d7b44e4b00fe3ad43cd295 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 14 Feb 2025 14:37:53 -0500 Subject: [PATCH 082/129] Change permissions tests to expected user list behavior --- sasdata/fair_database/fair_database/test_permissions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index f13c6745..9bf91d04 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -90,7 +90,8 @@ def test_list_authenticated_2(self): response.data, {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}, ) - self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response2.status_code, status.HTTP_200_OK) + self.assertEqual(response2.data, {"user_data_ids": {3: "cyl_testdata.txt"}}) self.assertEqual(response3.data, {"user_data_ids": {}}) # Unauthenticated user can view list of public data @@ -101,7 +102,8 @@ def test_list_unauthenticated(self): response.data, {"public_data_ids": {1: "cyl_400_40.txt", 3: "cyl_testdata.txt"}}, ) - self.assertEqual(response2.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response2.status_code, status.HTTP_200_OK) + self.assertEqual(response2.data, {"user_data_ids": {3: "cyl_testdata.txt"}}) # Authenticated user can load public data and owned private data def test_load_authenticated(self): From 44dd0d02be39de9a6c0c99a3bd1c642ae6fbc672 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 14 Feb 2025 14:44:24 -0500 Subject: [PATCH 083/129] Change data tests to expected user list behavior --- sasdata/fair_database/data/tests.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 330c7ff4..10ebede8 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -44,10 +44,16 @@ def test_does_list_user(self): request = self.client.get("/v1/data/list/testUser/", user=self.user) self.assertEqual(request.data, {"user_data_ids": {3: "cyl_400_20.txt"}}) - # Test list a nonexistent user's data + # Test list another user's public data def test_list_other_user(self): + client2 = APIClient() + request = client2.get("/v1/data/list/testUser/", user=self.user) + self.assertEqual(request.data, {"user_data_ids": {}}) + + # Test list a nonexistent user's data + def test_list_nonexistent_user(self): request = self.client.get("/v1/data/list/fakeUser/") - self.assertEqual(request.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(request.status_code, status.HTTP_404_NOT_FOUND) # Test loading a public data file def test_does_load_data_info_public(self): From ebaecadc331c5a2b6d851222ab507f78a215e04a Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 17 Feb 2025 14:15:13 -0500 Subject: [PATCH 084/129] Create initial version of dataset model --- sasdata/fair_database/data/models.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 7a563108..97a63acb 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -39,7 +39,27 @@ class DataFile(Data): ) -"""Database model for a set of data and associated metadata.""" +class DataSet(Data): + """Database model for a set of data and associated metadata.""" + + # dataset name + name = models.CharField() + + # associated files + files = models.ManyToManyField(DataFile) + + # ordinate + ordinate = models.JSONField() + + # abscissae + abscissae = models.JSONField() + + # data contents + data_contents = models.JSONField() + + # metadata + raw_metadata = models.JSONField() + """Database model for group of DataSets associated by a varying parameter.""" From 79ec06b2d3a35b3044760afa271e5b9c3cc33ca2 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 17 Feb 2025 15:11:51 -0500 Subject: [PATCH 085/129] Start OperationTree model --- sasdata/fair_database/data/models.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 97a63acb..0dac9170 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -63,6 +63,16 @@ class DataSet(Data): """Database model for group of DataSets associated by a varying parameter.""" -"""Database model for tree of operations performed on a DataSet.""" + +class OperationTree(Data): + """Database model for tree of operations performed on a DataSet.""" + + # Dataset the operation tree is performed on + dataset = models.ForeignKey(DataSet) + + # operation + + # previous operation + """Database model for a project save state.""" From 0d3a9d5bdf0fc993822bb727f733aee3672709d9 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 17 Feb 2025 15:13:55 -0500 Subject: [PATCH 086/129] Start Session model --- sasdata/fair_database/data/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 0dac9170..bfabb8c4 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -75,4 +75,11 @@ class OperationTree(Data): # previous operation -"""Database model for a project save state.""" +class Session(Data): + """Database model for a project save state.""" + + # dataset + dataset = models.ForeignKey(DataSet) + + # operation tree + operations = models.ForeignKey(OperationTree) From ee84c0d1b541f8a1d024bd4348b585e218e61237 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 19 Feb 2025 13:42:29 -0500 Subject: [PATCH 087/129] Begin SasData serializers --- sasdata/data.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/sasdata/data.py b/sasdata/data.py index 544ba27d..d1886031 100644 --- a/sasdata/data.py +++ b/sasdata/data.py @@ -1,3 +1,4 @@ +import json from enum import Enum from typing import TypeVar, Any, Self from dataclasses import dataclass @@ -42,4 +43,20 @@ def summary(self, indent = " ", include_raw=False): if include_raw: s += key_tree(self._raw_metadata) - return s \ No newline at end of file + return s + + def serialise(self) -> str: + return json.dumps(self._serialise_json()) + + def _serialise_json(self) -> dict[str, Any]: + return { + "name": self.name, + "data_contents": [], + "raw_metadata": {}, + "verbose": self._verbose, + "metadata": {}, + "ordinate": {}, + "abscissae": [], + "mask": {}, + "model_requirements": {} + } \ No newline at end of file From f2f9d6652c56715ef12c3f7421ae1aec81c80073 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 19 Feb 2025 15:54:48 -0500 Subject: [PATCH 088/129] Start of Quantity serializer --- sasdata/quantities/quantity.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sasdata/quantities/quantity.py b/sasdata/quantities/quantity.py index 584f3cf2..6faacf3a 100644 --- a/sasdata/quantities/quantity.py +++ b/sasdata/quantities/quantity.py @@ -1175,6 +1175,16 @@ def in_si_with_standard_error(self): else: return self.in_si(), None + # TODO: fill out actual values + def _serialise_json(self): + return { + "value": "", # figure out QuantityType serialisation + "units": "", # Unit serialisation + "standard_error": "", # also QuantityType serialisation + "hash_seed": self._hash_seed, # is this just a string? + "history": {} # QuantityHistory serializer + } + def __mul__(self: Self, other: ArrayLike | Self ) -> Self: if isinstance(other, Quantity): return DerivedQuantity( From 5527ef9ccdd715822e53d34aa5c412d105d55f3c Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 19 Feb 2025 16:07:53 -0500 Subject: [PATCH 089/129] Serializer for NamedQuantity --- sasdata/quantities/quantity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sasdata/quantities/quantity.py b/sasdata/quantities/quantity.py index 6faacf3a..98406485 100644 --- a/sasdata/quantities/quantity.py +++ b/sasdata/quantities/quantity.py @@ -1420,6 +1420,10 @@ def with_standard_error(self, standard_error: Quantity): raise UnitError(f"Standard error units ({standard_error.units}) " f"are not compatible with value units ({self.units})") + def _serialise_json(self): + quantity = super()._serialise_json() + quantity["name"] = self.name + return quantity @property def string_repr(self): From 42cb1c3e15b00ae034e6f7964bf860cd6a168af0 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 19 Feb 2025 16:09:28 -0500 Subject: [PATCH 090/129] Continue SasData serializer and add notes --- sasdata/data.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sasdata/data.py b/sasdata/data.py index d1886031..b6158c63 100644 --- a/sasdata/data.py +++ b/sasdata/data.py @@ -48,15 +48,16 @@ def summary(self, indent = " ", include_raw=False): def serialise(self) -> str: return json.dumps(self._serialise_json()) + # TODO: replace with serialization methods when written def _serialise_json(self) -> dict[str, Any]: return { "name": self.name, - "data_contents": [], - "raw_metadata": {}, + "data_contents": [q._serialise_json() for q in self._data_contents], + "raw_metadata": {}, # serialization for Groups and DataSets "verbose": self._verbose, - "metadata": {}, - "ordinate": {}, - "abscissae": [], + "metadata": {}, # serialization for MetaData + "ordinate": self.ordinate._serialise_json(), + "abscissae": [q._serialise_json() for q in self.abscissae], "mask": {}, "model_requirements": {} } \ No newline at end of file From 3bb54d8be8bd4125fabdfa304d6116c3691a01b7 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 19 Feb 2025 16:13:27 -0500 Subject: [PATCH 091/129] Add class for MetaData model --- sasdata/fair_database/data/models.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index bfabb8c4..41d500f8 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -48,6 +48,9 @@ class DataSet(Data): # associated files files = models.ManyToManyField(DataFile) + # metadata + metadata = models.ForeignKey("MetaData") + # ordinate ordinate = models.JSONField() @@ -61,6 +64,13 @@ class DataSet(Data): raw_metadata = models.JSONField() +class MetaData: + """Database model for scattering metadata""" + + # Associated data set + dataset = models.ForeignKey(DataSet) + + """Database model for group of DataSets associated by a varying parameter.""" From d039facad1c1f35bfe02f8362ac4bb15f7a13d64 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 20 Feb 2025 10:20:23 -0500 Subject: [PATCH 092/129] Temporarily comment out unmigrated models --- sasdata/fair_database/data/models.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 41d500f8..61ac7ed2 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -39,17 +39,18 @@ class DataFile(Data): ) +''' class DataSet(Data): """Database model for a set of data and associated metadata.""" # dataset name - name = models.CharField() + name = models.CharField(max_length=200) # associated files files = models.ManyToManyField(DataFile) # metadata - metadata = models.ForeignKey("MetaData") + # metadata = models.ForeignKey("MetaData", on_delete=models.CASCADE) # ordinate ordinate = models.JSONField() @@ -68,7 +69,7 @@ class MetaData: """Database model for scattering metadata""" # Associated data set - dataset = models.ForeignKey(DataSet) + # dataset = models.ForeignKey(DataSet, on_delete=models.CASCADE) """Database model for group of DataSets associated by a varying parameter.""" @@ -78,7 +79,7 @@ class OperationTree(Data): """Database model for tree of operations performed on a DataSet.""" # Dataset the operation tree is performed on - dataset = models.ForeignKey(DataSet) + # dataset = models.ForeignKey(DataSet, on_delete=models.CASCADE) # operation @@ -89,7 +90,8 @@ class Session(Data): """Database model for a project save state.""" # dataset - dataset = models.ForeignKey(DataSet) + # dataset = models.ForeignKey(DataSet, on_delete=models.CASCADE) # operation tree - operations = models.ForeignKey(OperationTree) + # operations = models.ForeignKey(OperationTree, on_delete=models.CASCADE) +''' From 8f332aa75593087c3559da4b31d3fa9ef68a75e2 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 20 Feb 2025 14:36:02 -0500 Subject: [PATCH 093/129] Serializers for units --- sasdata/quantities/_units_base.py | 21 +++++++++++++++++++-- sasdata/quantities/quantity.py | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 5a990ea2..39c4d3d1 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -12,11 +12,11 @@ class DimensionError(Exception): class Dimensions: """ - Note that some SI Base units are not useful from the perspecive of the sasview project, and make things + Note that some SI Base units are not useful from the perspective of the sasview project, and make things behave badly. In particular: moles and angular measures are dimensionless, and candelas are really a weighted measure of power. - We do however track angle and amount, because its really useful for formatting units + We do however track angle and amount, because it's really useful for formatting units """ def __init__(self, @@ -200,6 +200,17 @@ def si_repr(self): return ''.join(tokens) + def _serialise_json(self): + return { + "length": self.length, + "time": self.time, + "mass": self.mass, + "current": self.current, + "temperature": self.temperature, + "amount": self.moles_hint, + "angle": self.angle_hint + } + class Unit: def __init__(self, @@ -265,6 +276,12 @@ def __repr__(self): def parse(unit_string: str) -> "Unit": pass + def _serialise_json(self): + return { + "scale": self.scale, + "dimensions": self.dimensions._serialise_json() + } + class NamedUnit(Unit): """ Units, but they have a name, and a symbol diff --git a/sasdata/quantities/quantity.py b/sasdata/quantities/quantity.py index 98406485..bee2bcbe 100644 --- a/sasdata/quantities/quantity.py +++ b/sasdata/quantities/quantity.py @@ -1179,7 +1179,7 @@ def in_si_with_standard_error(self): def _serialise_json(self): return { "value": "", # figure out QuantityType serialisation - "units": "", # Unit serialisation + "units": self.units._serialise_json(), # Unit serialisation "standard_error": "", # also QuantityType serialisation "hash_seed": self._hash_seed, # is this just a string? "history": {} # QuantityHistory serializer From dd6ac8793aa4d3fe283715c074649acacc71b1fd Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 20 Feb 2025 14:45:55 -0500 Subject: [PATCH 094/129] QuantityHistory serializer --- sasdata/quantities/quantity.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sasdata/quantities/quantity.py b/sasdata/quantities/quantity.py index bee2bcbe..1de031b4 100644 --- a/sasdata/quantities/quantity.py +++ b/sasdata/quantities/quantity.py @@ -1085,6 +1085,15 @@ def summary(self): return s + def _serialise_json(self): + return { + "operation_tree": self.operation_tree.serialise(), + "references": { + key: self.references[key]._serialise_json() for key in self.references + } + + } + class Quantity[QuantityType]: @@ -1182,7 +1191,7 @@ def _serialise_json(self): "units": self.units._serialise_json(), # Unit serialisation "standard_error": "", # also QuantityType serialisation "hash_seed": self._hash_seed, # is this just a string? - "history": {} # QuantityHistory serializer + "history": self.history._serialise_json() } def __mul__(self: Self, other: ArrayLike | Self ) -> Self: From 9dcde3b38b1ba0182051fc079d454e0224292b48 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 20 Feb 2025 14:57:28 -0500 Subject: [PATCH 095/129] Serializers for Dataset and Group --- sasdata/data.py | 2 +- sasdata/data_backing.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/sasdata/data.py b/sasdata/data.py index b6158c63..ec62a606 100644 --- a/sasdata/data.py +++ b/sasdata/data.py @@ -53,7 +53,7 @@ def _serialise_json(self) -> dict[str, Any]: return { "name": self.name, "data_contents": [q._serialise_json() for q in self._data_contents], - "raw_metadata": {}, # serialization for Groups and DataSets + "raw_metadata": self._raw_metadata._serialise_json(), "verbose": self._verbose, "metadata": {}, # serialization for MetaData "ordinate": self.ordinate._serialise_json(), diff --git a/sasdata/data_backing.py b/sasdata/data_backing.py index 564f466a..c880c97d 100644 --- a/sasdata/data_backing.py +++ b/sasdata/data_backing.py @@ -36,6 +36,22 @@ def summary(self, indent_amount: int = 0, indent: str = " ") -> str: return s + def _serialise_json(self): + content = { + { + "name": self.name, + "data": "", # TODO: figure out QuantityType serialisation + "attributes": {} + } + } + for key in self.attributes: + value = self.attributes[key] + if isinstance(value, (Group, Dataset)): + content["attributes"]["key"] = value._serialise_json() + else: + content["attributes"]["key"] = value + return content + @dataclass class Group: name: str @@ -48,6 +64,16 @@ def summary(self, indent_amount: int=0, indent=" "): return s + def _serialise_json(self): + return { + { + "name": self.name, + "children": { + key: self.children[key]._serialise_json() for key in self.children + } + } + } + class Function: """ Representation of a (data driven) function, such as I vs Q """ From 1ff51647311ad84b55f4fbaf98ed362a894cd9d6 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 20 Feb 2025 15:15:09 -0500 Subject: [PATCH 096/129] Framework for metadata serializers --- sasdata/data.py | 2 +- sasdata/metadata.py | 51 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/sasdata/data.py b/sasdata/data.py index ec62a606..45f000af 100644 --- a/sasdata/data.py +++ b/sasdata/data.py @@ -55,7 +55,7 @@ def _serialise_json(self) -> dict[str, Any]: "data_contents": [q._serialise_json() for q in self._data_contents], "raw_metadata": self._raw_metadata._serialise_json(), "verbose": self._verbose, - "metadata": {}, # serialization for MetaData + "metadata": self.metadata._serialise_json(), "ordinate": self.ordinate._serialise_json(), "abscissae": [q._serialise_json() for q in self.abscissae], "mask": {}, diff --git a/sasdata/metadata.py b/sasdata/metadata.py index 3c29f33e..4d938d87 100644 --- a/sasdata/metadata.py +++ b/sasdata/metadata.py @@ -269,6 +269,18 @@ def summary(self) -> str: # # return _str + def _serialise_json(self): + return { + "name": "", + "sample_id": "", + "thickness": "", + "transmission": "", + "temperature": "", + "position": "", + "orientation": "", + "details": "" + } + class Process: """ @@ -300,6 +312,15 @@ def summary(self): f" Notes: {self.notes.value}\n" ) + def _serialise_json(self): + return { + "name": "", + "date": "", + "description": "", + "term": "", + "notes": "" + } + class TransmissionSpectrum: """ Class that holds information about transmission spectrum @@ -335,6 +356,15 @@ def summary(self) -> str: f" Wavelengths: {self.wavelength.value}\n" f" Transmission: {self.transmission.value}\n") + def _serialise_json(self): + return { + "name": "", + "timestamp": "", + "wavelength": "", + "transmission": "", + "transmission_deviation": "" + } + class Instrument: def __init__(self, target: AccessorTarget): @@ -350,6 +380,14 @@ def summary(self): self.detector.summary() + self.source.summary()) + def _serialise_json(self): + return { + "aperture": "", + "collimation": "", + "detector": "", + "source": "" + } + def decode_string(data): """ This is some crazy stuff""" @@ -401,4 +439,15 @@ def summary(self): self.process.summary() + self.sample.summary() + self.instrument.summary() + - self.transmission_spectrum.summary()) \ No newline at end of file + self.transmission_spectrum.summary()) + + def _serialise_json(self): + return { + "instrument": self.instrument._serialise_json(), + "process": self.process._serialise_json(), + "sample": self.sample._serialise_json(), + "transmission_spectrum": self.transmission_spectrum._serialise_json(), + "title": self.title, + "run": self.run, + "definition": self.definition + } \ No newline at end of file From f9eab4a0cd895a130815c76209fbe22f1ef18b6b Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 20 Feb 2025 15:32:07 -0500 Subject: [PATCH 097/129] Instrument metadata serializers --- sasdata/metadata.py | 48 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/sasdata/metadata.py b/sasdata/metadata.py index 4d938d87..907738b0 100644 --- a/sasdata/metadata.py +++ b/sasdata/metadata.py @@ -64,6 +64,17 @@ def summary(self): f" Pixel size: {self.pixel_size.value}\n" f" Slit length: {self.slit_length.value}\n") + def _serialise_json(self): + return { + "name": "", + "distance": "", + "offset": "", + "orientation": "", + "beam_center": "", + "pixel_size": "", + "slit_length": "" + } + class Aperture: @@ -97,6 +108,14 @@ def summary(self): f" Aperture size: {self.size.value}\n" f" Aperture distance: {self.distance.value}\n") + def _serialise_json(self): + return { + "name": "", + "type": "", + "size": "", + "distance": "" + } + class Collimation: """ Class to hold collimation information @@ -123,6 +142,12 @@ def summary(self): f"Collimation:\n" f" Length: {self.length.value}\n") + def _serialise_json(self): + return { + "name": "", + "length": "" + } + class Source: @@ -197,6 +222,21 @@ def summary(self) -> str: f" Wavelength Spread: {self.wavelength_spread.value}\n" f" Beam Size: {self.beam_size.value}\n") + def _serialise_json(self): + return { + "name": "", + "radiation": "", + "type": "", + "probe_particle": "", + "beam_size_name": "", + "beam_size": "", + "beam_shape": "", + "wavelength": "", + "wavelength_min": "", + "wavelength_max": "", + "wavelength_spread": "" + } + """ @@ -382,10 +422,10 @@ def summary(self): def _serialise_json(self): return { - "aperture": "", - "collimation": "", - "detector": "", - "source": "" + "aperture": self.aperture._serialise_json(), + "collimation": self.collimation._serialise_json(), + "detector": self.detector._serialise_json(), + "source": self.source._serialise_json() } def decode_string(data): From 41cdb20ae56ce892585a878093537e403adeefae Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 20 Feb 2025 15:53:26 -0500 Subject: [PATCH 098/129] Finish metadata serializers --- sasdata/metadata.py | 84 ++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/sasdata/metadata.py b/sasdata/metadata.py index 907738b0..4976dad2 100644 --- a/sasdata/metadata.py +++ b/sasdata/metadata.py @@ -66,13 +66,13 @@ def summary(self): def _serialise_json(self): return { - "name": "", - "distance": "", - "offset": "", - "orientation": "", - "beam_center": "", - "pixel_size": "", - "slit_length": "" + "name": self.name.value, + "distance": self.distance.value._serialise_json(), + "offset": self.offset.value._serialise_json(), + "orientation": self.orientation.value._serialise_json(), + "beam_center": self.beam_center.value._serialise_json(), + "pixel_size": self.pixel_size.value._serialise_json(), + "slit_length": self.slit_length.value._serialise_json() } @@ -110,10 +110,10 @@ def summary(self): def _serialise_json(self): return { - "name": "", - "type": "", - "size": "", - "distance": "" + "name": self.name.value, + "type": self.type.value, + "size": self.size.value._serialise_json(), + "distance": self.distance.value._serialise_json() } class Collimation: @@ -144,8 +144,8 @@ def summary(self): def _serialise_json(self): return { - "name": "", - "length": "" + "name": self.name.value, + "length": self.length.value._serialise_json() } @@ -224,17 +224,17 @@ def summary(self) -> str: def _serialise_json(self): return { - "name": "", - "radiation": "", - "type": "", - "probe_particle": "", - "beam_size_name": "", - "beam_size": "", - "beam_shape": "", - "wavelength": "", - "wavelength_min": "", - "wavelength_max": "", - "wavelength_spread": "" + "name": self.name.value, + "radiation": self.radiation.value, + "type": self.type.value, + "probe_particle": self.probe_particle.value, + "beam_size_name": self.beam_size_name.value, + "beam_size": self.beam_size.value._serialise_json(), + "beam_shape": self.beam_shape.value, + "wavelength": self.wavelength.value._serialise_json(), + "wavelength_min": self.wavelength_min.value._serialise_json(), + "wavelength_max": self.wavelength_max.value._serialise_json(), + "wavelength_spread": self.wavelength_spread.value._serialise_json() } @@ -311,14 +311,14 @@ def summary(self) -> str: def _serialise_json(self): return { - "name": "", - "sample_id": "", - "thickness": "", - "transmission": "", - "temperature": "", - "position": "", - "orientation": "", - "details": "" + "name": self.name.value, + "sample_id": self.sample_id.value, + "thickness": self.thickness.value._serialise_json(), + "transmission": self.transmission.value, + "temperature": self.temperature.value._serialise_json(), + "position": self.position.value._serialise_json(), + "orientation": self.orientation.value._serialise_json(), + "details": self.details.value } @@ -354,11 +354,11 @@ def summary(self): def _serialise_json(self): return { - "name": "", - "date": "", - "description": "", - "term": "", - "notes": "" + "name": self.name.value, + "date": self.date.value, + "description": self.description.value, + "term": self.term.value, + "notes": self.notes.value } class TransmissionSpectrum: @@ -398,11 +398,11 @@ def summary(self) -> str: def _serialise_json(self): return { - "name": "", - "timestamp": "", - "wavelength": "", - "transmission": "", - "transmission_deviation": "" + "name": self.name.value, + "timestamp": self.timestamp.value, + "wavelength": self.wavelength.value._serialise_json(), + "transmission": self.transmission.value._serialise_json(), + "transmission_deviation": self.transmission_deviation.value._serialise_json() } From a34451db8ed65ca8284f961168bb5c423bbf13da Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Tue, 25 Feb 2025 16:30:00 -0500 Subject: [PATCH 099/129] Updates to in-progess data models --- sasdata/fair_database/data/models.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 61ac7ed2..5c007975 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -64,6 +64,20 @@ class DataSet(Data): # metadata raw_metadata = models.JSONField() +class Quantity(): + + # data value + value = models.JSONField() + + # variance of the data + variance = models.JSONField() + + # units + units = models.CharField(max_length=200) + + hash = IntegerField() # this might change + + class MetaData: """Database model for scattering metadata""" @@ -84,8 +98,10 @@ class OperationTree(Data): # operation # previous operation + parent_operation = models.ForeignKey("self", blank=True, null=True) +''' - +''' class Session(Data): """Database model for a project save state.""" From ec514c7349679bd9b11bb234607e7dd6739c8e27 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 26 Feb 2025 11:11:02 -0500 Subject: [PATCH 100/129] Initial versions of phase 1 models --- sasdata/fair_database/data/models.py | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 5c007975..f2c54b33 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -6,7 +6,7 @@ class Data(models.Model): """Base model for data.""" - # username + # owner of the data current_user = models.ForeignKey( User, blank=True, null=True, on_delete=models.CASCADE ) @@ -39,7 +39,6 @@ class DataFile(Data): ) -''' class DataSet(Data): """Database model for a set of data and associated metadata.""" @@ -50,21 +49,23 @@ class DataSet(Data): files = models.ManyToManyField(DataFile) # metadata - # metadata = models.ForeignKey("MetaData", on_delete=models.CASCADE) + metadata = models.ForeignKey("MetaData", on_delete=models.CASCADE) # ordinate - ordinate = models.JSONField() + # ordinate = models.JSONField() # abscissae - abscissae = models.JSONField() + # abscissae = models.JSONField() # data contents - data_contents = models.JSONField() + # data_contents = models.JSONField() # metadata - raw_metadata = models.JSONField() + # raw_metadata = models.JSONField() + -class Quantity(): +class Quantity: + """Database model for data quantities such as the ordinate and abscissae.""" # data value value = models.JSONField() @@ -75,31 +76,31 @@ class Quantity(): # units units = models.CharField(max_length=200) - hash = IntegerField() # this might change - + # hash value + hash = models.IntegerField() class MetaData: """Database model for scattering metadata""" # Associated data set - # dataset = models.ForeignKey(DataSet, on_delete=models.CASCADE) + dataset = models.ForeignKey(DataSet, on_delete=models.CASCADE) """Database model for group of DataSets associated by a varying parameter.""" -class OperationTree(Data): +class OperationTree: """Database model for tree of operations performed on a DataSet.""" # Dataset the operation tree is performed on - # dataset = models.ForeignKey(DataSet, on_delete=models.CASCADE) + dataset = models.ForeignKey(DataSet, on_delete=models.CASCADE) # operation # previous operation parent_operation = models.ForeignKey("self", blank=True, null=True) -''' + ''' class Session(Data): From d9e11e511c64382ac7e3c5a2e2751bb4b126186e Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 26 Feb 2025 11:36:29 -0500 Subject: [PATCH 101/129] model migrations --- ...aset_metadata_dataset_metadata_and_more.py | 127 ++++++++++++++++++ sasdata/fair_database/data/models.py | 18 ++- 2 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 sasdata/fair_database/data/migrations/0004_quantity_dataset_metadata_dataset_metadata_and_more.py diff --git a/sasdata/fair_database/data/migrations/0004_quantity_dataset_metadata_dataset_metadata_and_more.py b/sasdata/fair_database/data/migrations/0004_quantity_dataset_metadata_dataset_metadata_and_more.py new file mode 100644 index 00000000..e56dc51d --- /dev/null +++ b/sasdata/fair_database/data/migrations/0004_quantity_dataset_metadata_dataset_metadata_and_more.py @@ -0,0 +1,127 @@ +# Generated by Django 5.1.6 on 2025-02-26 16:34 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("data", "0003_alter_datafile_is_public"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Quantity", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.JSONField()), + ("variance", models.JSONField()), + ("units", models.CharField(max_length=200)), + ("hash", models.IntegerField()), + ], + ), + migrations.CreateModel( + name="DataSet", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "is_public", + models.BooleanField( + default=False, help_text="opt in to make your data public" + ), + ), + ("name", models.CharField(max_length=200)), + ( + "current_user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ("files", models.ManyToManyField(to="data.datafile")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="MetaData", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dataset", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="associated_data", + to="data.dataset", + ), + ), + ], + ), + migrations.AddField( + model_name="dataset", + name="metadata", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="associated_metadata", + to="data.metadata", + ), + ), + migrations.CreateModel( + name="OperationTree", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "dataset", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="data.dataset" + ), + ), + ( + "parent_operation", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="data.operationtree", + ), + ), + ], + ), + ] diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index f2c54b33..69066fb1 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -49,7 +49,9 @@ class DataSet(Data): files = models.ManyToManyField(DataFile) # metadata - metadata = models.ForeignKey("MetaData", on_delete=models.CASCADE) + metadata = models.OneToOneField( + "MetaData", on_delete=models.CASCADE, related_name="associated_metadata" + ) # ordinate # ordinate = models.JSONField() @@ -64,7 +66,7 @@ class DataSet(Data): # raw_metadata = models.JSONField() -class Quantity: +class Quantity(models.Model): """Database model for data quantities such as the ordinate and abscissae.""" # data value @@ -80,17 +82,19 @@ class Quantity: hash = models.IntegerField() -class MetaData: +class MetaData(models.Model): """Database model for scattering metadata""" # Associated data set - dataset = models.ForeignKey(DataSet, on_delete=models.CASCADE) + dataset = models.OneToOneField( + "DataSet", on_delete=models.CASCADE, related_name="associated_data" + ) """Database model for group of DataSets associated by a varying parameter.""" -class OperationTree: +class OperationTree(models.Model): """Database model for tree of operations performed on a DataSet.""" # Dataset the operation tree is performed on @@ -99,7 +103,9 @@ class OperationTree: # operation # previous operation - parent_operation = models.ForeignKey("self", blank=True, null=True) + parent_operation = models.ForeignKey( + "self", blank=True, null=True, on_delete=models.CASCADE + ) ''' From b0a0f91c46a2b1e23d4052ec10aeaad174b07136 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 26 Feb 2025 11:55:16 -0500 Subject: [PATCH 102/129] Add field for authorized non-owners for data --- ...t_datafile_users_dataset_users_and_more.py | 58 +++++++++++++++++++ sasdata/fair_database/data/models.py | 14 ++--- 2 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 sasdata/fair_database/data/migrations/0005_remove_metadata_dataset_datafile_users_dataset_users_and_more.py diff --git a/sasdata/fair_database/data/migrations/0005_remove_metadata_dataset_datafile_users_dataset_users_and_more.py b/sasdata/fair_database/data/migrations/0005_remove_metadata_dataset_datafile_users_dataset_users_and_more.py new file mode 100644 index 00000000..3a333c2f --- /dev/null +++ b/sasdata/fair_database/data/migrations/0005_remove_metadata_dataset_datafile_users_dataset_users_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 5.1.6 on 2025-02-26 16:54 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("data", "0004_quantity_dataset_metadata_dataset_metadata_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name="metadata", + name="dataset", + ), + migrations.AddField( + model_name="datafile", + name="users", + field=models.ManyToManyField(related_name="+", to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name="dataset", + name="users", + field=models.ManyToManyField(related_name="+", to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name="datafile", + name="current_user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="dataset", + name="current_user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="dataset", + name="metadata", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="data.metadata" + ), + ), + ] diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 69066fb1..1393d05a 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -8,9 +8,11 @@ class Data(models.Model): # owner of the data current_user = models.ForeignKey( - User, blank=True, null=True, on_delete=models.CASCADE + User, blank=True, null=True, on_delete=models.CASCADE, related_name="+" ) + users = models.ManyToManyField(User, related_name="+") + # is the data public? is_public = models.BooleanField( default=False, help_text="opt in to make your data public" @@ -49,9 +51,7 @@ class DataSet(Data): files = models.ManyToManyField(DataFile) # metadata - metadata = models.OneToOneField( - "MetaData", on_delete=models.CASCADE, related_name="associated_metadata" - ) + metadata = models.OneToOneField("MetaData", on_delete=models.CASCADE) # ordinate # ordinate = models.JSONField() @@ -86,9 +86,9 @@ class MetaData(models.Model): """Database model for scattering metadata""" # Associated data set - dataset = models.OneToOneField( - "DataSet", on_delete=models.CASCADE, related_name="associated_data" - ) + # dataset = models.OneToOneField( + # "DataSet", on_delete=models.CASCADE, related_name="associated_data" + # ) """Database model for group of DataSets associated by a varying parameter.""" From df27fdcd884054e12f4f57c3611b5d69e61fe84c Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 26 Feb 2025 12:50:27 -0500 Subject: [PATCH 103/129] Change permissions to check users with access instead of just ownership --- sasdata/fair_database/fair_database/permissions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sasdata/fair_database/fair_database/permissions.py b/sasdata/fair_database/fair_database/permissions.py index e62e7257..3ce03f0c 100644 --- a/sasdata/fair_database/fair_database/permissions.py +++ b/sasdata/fair_database/fair_database/permissions.py @@ -5,10 +5,14 @@ def is_owner(request, obj): return request.user.is_authenticated and request.user == obj.current_user +def has_access(request, obj): + return request.user.is_authenticated and request.user in obj.users.all() + + class DataPermission(BasePermission): def has_object_permission(self, request, view, obj): if request.method == "GET": - if obj.is_public or is_owner(request, obj): + if obj.is_public or has_access(request, obj): return True elif request.method == "DELETE": if obj.is_private and is_owner(request, obj): @@ -19,7 +23,7 @@ def has_object_permission(self, request, view, obj): def check_permissions(request, obj): if request.method == "GET": - if obj.is_public or is_owner(request, obj): + if obj.is_public or has_access(request, obj): return True elif request.method == "DELETE": if obj.is_private and is_owner(request, obj): From 9ef77446d08a47a61c7b69d58401528c2c759912 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 26 Feb 2025 16:08:32 -0500 Subject: [PATCH 104/129] Ensure owner can access data --- ...lter_datafile_users_alter_dataset_users.py | 28 +++++++++++++++++++ ...lter_datafile_users_alter_dataset_users.py | 28 +++++++++++++++++++ ...lter_datafile_users_alter_dataset_users.py | 28 +++++++++++++++++++ sasdata/fair_database/data/models.py | 2 +- sasdata/fair_database/data/views.py | 2 ++ .../fair_database/permissions.py | 6 +++- 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 sasdata/fair_database/data/migrations/0006_alter_datafile_users_alter_dataset_users.py create mode 100644 sasdata/fair_database/data/migrations/0007_alter_datafile_users_alter_dataset_users.py create mode 100644 sasdata/fair_database/data/migrations/0008_alter_datafile_users_alter_dataset_users.py diff --git a/sasdata/fair_database/data/migrations/0006_alter_datafile_users_alter_dataset_users.py b/sasdata/fair_database/data/migrations/0006_alter_datafile_users_alter_dataset_users.py new file mode 100644 index 00000000..4d2ee4de --- /dev/null +++ b/sasdata/fair_database/data/migrations/0006_alter_datafile_users_alter_dataset_users.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.6 on 2025-02-26 20:53 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("data", "0005_remove_metadata_dataset_datafile_users_dataset_users_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="datafile", + name="users", + field=models.ManyToManyField( + default=[], related_name="+", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="dataset", + name="users", + field=models.ManyToManyField( + default=[], related_name="+", to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/sasdata/fair_database/data/migrations/0007_alter_datafile_users_alter_dataset_users.py b/sasdata/fair_database/data/migrations/0007_alter_datafile_users_alter_dataset_users.py new file mode 100644 index 00000000..6540d4e7 --- /dev/null +++ b/sasdata/fair_database/data/migrations/0007_alter_datafile_users_alter_dataset_users.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.6 on 2025-02-26 21:03 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("data", "0006_alter_datafile_users_alter_dataset_users"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="datafile", + name="users", + field=models.ManyToManyField( + blank=True, default=[], related_name="+", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="dataset", + name="users", + field=models.ManyToManyField( + blank=True, default=[], related_name="+", to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/sasdata/fair_database/data/migrations/0008_alter_datafile_users_alter_dataset_users.py b/sasdata/fair_database/data/migrations/0008_alter_datafile_users_alter_dataset_users.py new file mode 100644 index 00000000..56dbb177 --- /dev/null +++ b/sasdata/fair_database/data/migrations/0008_alter_datafile_users_alter_dataset_users.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.6 on 2025-02-26 21:04 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("data", "0007_alter_datafile_users_alter_dataset_users"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="datafile", + name="users", + field=models.ManyToManyField( + blank=True, related_name="+", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AlterField( + model_name="dataset", + name="users", + field=models.ManyToManyField( + blank=True, related_name="+", to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 1393d05a..2ad7b664 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -11,7 +11,7 @@ class Data(models.Model): User, blank=True, null=True, on_delete=models.CASCADE, related_name="+" ) - users = models.ManyToManyField(User, related_name="+") + users = models.ManyToManyField(User, blank=True, related_name="+") # is the data public? is_public = models.BooleanField( diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index fcc782c5..ce278589 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -70,6 +70,7 @@ def upload(request, data_id=None, version=None): data={ "file_name": os.path.basename(form.instance.file.path), "current_user": request.user.id, + "users": [request.user.id], }, context={"is_public": db.is_public}, ) @@ -79,6 +80,7 @@ def upload(request, data_id=None, version=None): data={ "file_name": os.path.basename(form.instance.file.path), "current_user": None, + "users": [], }, context={"is_public": db.is_public}, ) diff --git a/sasdata/fair_database/fair_database/permissions.py b/sasdata/fair_database/fair_database/permissions.py index 3ce03f0c..224e4c00 100644 --- a/sasdata/fair_database/fair_database/permissions.py +++ b/sasdata/fair_database/fair_database/permissions.py @@ -6,7 +6,11 @@ def is_owner(request, obj): def has_access(request, obj): - return request.user.is_authenticated and request.user in obj.users.all() + return ( + is_owner(request, obj) + or request.user.is_authenticated + and request.user in obj.users.all() + ) class DataPermission(BasePermission): From cc51d3472ba35e73557a8786442d4d93224c776f Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 26 Feb 2025 16:14:50 -0500 Subject: [PATCH 105/129] Bare bones serializers for database models --- sasdata/fair_database/data/serializers.py | 26 ++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/serializers.py b/sasdata/fair_database/data/serializers.py index 94c1f4e4..8e9c2974 100644 --- a/sasdata/fair_database/data/serializers.py +++ b/sasdata/fair_database/data/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from data.models import DataFile +from data.models import DataFile, DataSet, MetaData, OperationTree, Quantity class DataFileSerializer(serializers.ModelSerializer): @@ -12,3 +12,27 @@ def validate(self, data): if not self.context["is_public"] and not data["current_user"]: raise serializers.ValidationError("private data must have an owner") return data + + +class DataSetSerializer(serializers.ModelSerializer): + class Meta: + model = DataSet + fields = "__all__" + + +class QuantitySerializer(serializers.ModelSerializer): + class Meta: + model = Quantity + fields = "__all__" + + +class MetaDataSerializer(serializers.ModelSerializer): + class Meta: + model = MetaData + fields = "__all__" + + +class OperationTreeSerializer(serializers.ModelSerializer): + class Meta: + model = OperationTree + fields = "__all__" From 3100644fdf8a19c257312fa3de0522b3170824ac Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 27 Feb 2025 14:32:21 -0500 Subject: [PATCH 106/129] Switch serialization methods to non-protected so pycharm stops nagging me --- sasdata/data_backing.py | 8 ++-- sasdata/metadata.py | 76 +++++++++++++++---------------- sasdata/quantities/_units_base.py | 6 +-- sasdata/quantities/quantity.py | 12 ++--- 4 files changed, 51 insertions(+), 51 deletions(-) diff --git a/sasdata/data_backing.py b/sasdata/data_backing.py index c880c97d..0504963e 100644 --- a/sasdata/data_backing.py +++ b/sasdata/data_backing.py @@ -36,7 +36,7 @@ def summary(self, indent_amount: int = 0, indent: str = " ") -> str: return s - def _serialise_json(self): + def serialise_json(self): content = { { "name": self.name, @@ -47,7 +47,7 @@ def _serialise_json(self): for key in self.attributes: value = self.attributes[key] if isinstance(value, (Group, Dataset)): - content["attributes"]["key"] = value._serialise_json() + content["attributes"]["key"] = value.serialise_json() else: content["attributes"]["key"] = value return content @@ -64,12 +64,12 @@ def summary(self, indent_amount: int=0, indent=" "): return s - def _serialise_json(self): + def serialise_json(self): return { { "name": self.name, "children": { - key: self.children[key]._serialise_json() for key in self.children + key: self.children[key].serialise_json() for key in self.children } } } diff --git a/sasdata/metadata.py b/sasdata/metadata.py index 4976dad2..a95f9024 100644 --- a/sasdata/metadata.py +++ b/sasdata/metadata.py @@ -64,15 +64,15 @@ def summary(self): f" Pixel size: {self.pixel_size.value}\n" f" Slit length: {self.slit_length.value}\n") - def _serialise_json(self): + def serialise_json(self): return { "name": self.name.value, - "distance": self.distance.value._serialise_json(), - "offset": self.offset.value._serialise_json(), - "orientation": self.orientation.value._serialise_json(), - "beam_center": self.beam_center.value._serialise_json(), - "pixel_size": self.pixel_size.value._serialise_json(), - "slit_length": self.slit_length.value._serialise_json() + "distance": self.distance.value.serialise_json(), + "offset": self.offset.value.serialise_json(), + "orientation": self.orientation.value.serialise_json(), + "beam_center": self.beam_center.value.serialise_json(), + "pixel_size": self.pixel_size.value.serialise_json(), + "slit_length": self.slit_length.value.serialise_json() } @@ -108,12 +108,12 @@ def summary(self): f" Aperture size: {self.size.value}\n" f" Aperture distance: {self.distance.value}\n") - def _serialise_json(self): + def serialise_json(self): return { "name": self.name.value, "type": self.type.value, - "size": self.size.value._serialise_json(), - "distance": self.distance.value._serialise_json() + "size": self.size.value.serialise_json(), + "distance": self.distance.value.serialise_json() } class Collimation: @@ -142,10 +142,10 @@ def summary(self): f"Collimation:\n" f" Length: {self.length.value}\n") - def _serialise_json(self): + def serialise_json(self): return { "name": self.name.value, - "length": self.length.value._serialise_json() + "length": self.length.value.serialise_json() } @@ -222,19 +222,19 @@ def summary(self) -> str: f" Wavelength Spread: {self.wavelength_spread.value}\n" f" Beam Size: {self.beam_size.value}\n") - def _serialise_json(self): + def serialise_json(self): return { "name": self.name.value, "radiation": self.radiation.value, "type": self.type.value, "probe_particle": self.probe_particle.value, "beam_size_name": self.beam_size_name.value, - "beam_size": self.beam_size.value._serialise_json(), + "beam_size": self.beam_size.value.serialise_json(), "beam_shape": self.beam_shape.value, - "wavelength": self.wavelength.value._serialise_json(), - "wavelength_min": self.wavelength_min.value._serialise_json(), - "wavelength_max": self.wavelength_max.value._serialise_json(), - "wavelength_spread": self.wavelength_spread.value._serialise_json() + "wavelength": self.wavelength.value.serialise_json(), + "wavelength_min": self.wavelength_min.value.serialise_json(), + "wavelength_max": self.wavelength_max.value.serialise_json(), + "wavelength_spread": self.wavelength_spread.value.serialise_json() } @@ -309,15 +309,15 @@ def summary(self) -> str: # # return _str - def _serialise_json(self): + def serialise_json(self): return { "name": self.name.value, "sample_id": self.sample_id.value, - "thickness": self.thickness.value._serialise_json(), + "thickness": self.thickness.value.serialise_json(), "transmission": self.transmission.value, - "temperature": self.temperature.value._serialise_json(), - "position": self.position.value._serialise_json(), - "orientation": self.orientation.value._serialise_json(), + "temperature": self.temperature.value.serialise_json(), + "position": self.position.value.serialise_json(), + "orientation": self.orientation.value.serialise_json(), "details": self.details.value } @@ -352,7 +352,7 @@ def summary(self): f" Notes: {self.notes.value}\n" ) - def _serialise_json(self): + def serialise_json(self): return { "name": self.name.value, "date": self.date.value, @@ -396,13 +396,13 @@ def summary(self) -> str: f" Wavelengths: {self.wavelength.value}\n" f" Transmission: {self.transmission.value}\n") - def _serialise_json(self): + def serialise_json(self): return { "name": self.name.value, "timestamp": self.timestamp.value, - "wavelength": self.wavelength.value._serialise_json(), - "transmission": self.transmission.value._serialise_json(), - "transmission_deviation": self.transmission_deviation.value._serialise_json() + "wavelength": self.wavelength.value.serialise_json(), + "transmission": self.transmission.value.serialise_json(), + "transmission_deviation": self.transmission_deviation.value.serialise_json() } @@ -420,12 +420,12 @@ def summary(self): self.detector.summary() + self.source.summary()) - def _serialise_json(self): + def serialise_json(self): return { - "aperture": self.aperture._serialise_json(), - "collimation": self.collimation._serialise_json(), - "detector": self.detector._serialise_json(), - "source": self.source._serialise_json() + "aperture": self.aperture.serialise_json(), + "collimation": self.collimation.serialise_json(), + "detector": self.detector.serialise_json(), + "source": self.source.serialise_json() } def decode_string(data): @@ -481,12 +481,12 @@ def summary(self): self.instrument.summary() + self.transmission_spectrum.summary()) - def _serialise_json(self): + def serialise_json(self): return { - "instrument": self.instrument._serialise_json(), - "process": self.process._serialise_json(), - "sample": self.sample._serialise_json(), - "transmission_spectrum": self.transmission_spectrum._serialise_json(), + "instrument": self.instrument.serialise_json(), + "process": self.process.serialise_json(), + "sample": self.sample.serialise_json(), + "transmission_spectrum": self.transmission_spectrum.serialise_json(), "title": self.title, "run": self.run, "definition": self.definition diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 39c4d3d1..72e92bdd 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -200,7 +200,7 @@ def si_repr(self): return ''.join(tokens) - def _serialise_json(self): + def serialise_json(self): return { "length": self.length, "time": self.time, @@ -276,10 +276,10 @@ def __repr__(self): def parse(unit_string: str) -> "Unit": pass - def _serialise_json(self): + def serialise_json(self): return { "scale": self.scale, - "dimensions": self.dimensions._serialise_json() + "dimensions": self.dimensions.serialise_json() } class NamedUnit(Unit): diff --git a/sasdata/quantities/quantity.py b/sasdata/quantities/quantity.py index 1de031b4..749bd4e5 100644 --- a/sasdata/quantities/quantity.py +++ b/sasdata/quantities/quantity.py @@ -1085,11 +1085,11 @@ def summary(self): return s - def _serialise_json(self): + def serialise_json(self): return { "operation_tree": self.operation_tree.serialise(), "references": { - key: self.references[key]._serialise_json() for key in self.references + key: self.references[key].serialise_json() for key in self.references } } @@ -1185,13 +1185,13 @@ def in_si_with_standard_error(self): return self.in_si(), None # TODO: fill out actual values - def _serialise_json(self): + def serialise_json(self): return { "value": "", # figure out QuantityType serialisation - "units": self.units._serialise_json(), # Unit serialisation + "units": self.units.serialise_json(), # Unit serialisation "standard_error": "", # also QuantityType serialisation "hash_seed": self._hash_seed, # is this just a string? - "history": self.history._serialise_json() + "history": self.history.serialise_json() } def __mul__(self: Self, other: ArrayLike | Self ) -> Self: @@ -1429,7 +1429,7 @@ def with_standard_error(self, standard_error: Quantity): raise UnitError(f"Standard error units ({standard_error.units}) " f"are not compatible with value units ({self.units})") - def _serialise_json(self): + def serialise_json(self): quantity = super()._serialise_json() quantity["name"] = self.name return quantity From fd20a4fcb526642527422ed157362f53e0c4cba4 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 27 Feb 2025 14:32:52 -0500 Subject: [PATCH 107/129] Deserialization for SasData class --- sasdata/data.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/sasdata/data.py b/sasdata/data.py index 45f000af..85b2eb96 100644 --- a/sasdata/data.py +++ b/sasdata/data.py @@ -45,6 +45,17 @@ def summary(self, indent = " ", include_raw=False): return s + def deserialise(self, data: str) -> "SasData": + json_data = json.loads(data) + return self.deserialise_json(json_data) + + def deserialise_json(self, json_data: dict) -> "SasData": + name = json_data["name"] + data_contents = [] # deserialise Quantity + raw_metadata = None # deserialise Group + verbose = json_data["verbose"] + return SasData(name, data_contents, raw_metadata, verbose) + def serialise(self) -> str: return json.dumps(self._serialise_json()) @@ -52,12 +63,12 @@ def serialise(self) -> str: def _serialise_json(self) -> dict[str, Any]: return { "name": self.name, - "data_contents": [q._serialise_json() for q in self._data_contents], - "raw_metadata": self._raw_metadata._serialise_json(), + "data_contents": [q.serialise_json() for q in self._data_contents], + "raw_metadata": self._raw_metadata.serialise_json(), "verbose": self._verbose, - "metadata": self.metadata._serialise_json(), - "ordinate": self.ordinate._serialise_json(), - "abscissae": [q._serialise_json() for q in self.abscissae], + "metadata": self.metadata.serialise_json(), + "ordinate": self.ordinate.serialise_json(), + "abscissae": [q.serialise_json() for q in self.abscissae], "mask": {}, "model_requirements": {} } \ No newline at end of file From d21f4f61e0b53929e1cf89240223f350deb28827 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 27 Feb 2025 15:00:03 -0500 Subject: [PATCH 108/129] Deserialization for Group and Dataset classes --- sasdata/data.py | 12 ++++++----- sasdata/data_backing.py | 45 +++++++++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/sasdata/data.py b/sasdata/data.py index 85b2eb96..3961bdd5 100644 --- a/sasdata/data.py +++ b/sasdata/data.py @@ -45,14 +45,16 @@ def summary(self, indent = " ", include_raw=False): return s - def deserialise(self, data: str) -> "SasData": + @staticmethod + def deserialise(data: str) -> "SasData": json_data = json.loads(data) - return self.deserialise_json(json_data) + return SasData.deserialise_json(json_data) - def deserialise_json(self, json_data: dict) -> "SasData": + @staticmethod + def deserialise_json(json_data: dict) -> "SasData": name = json_data["name"] - data_contents = [] # deserialise Quantity - raw_metadata = None # deserialise Group + data_contents = [] # deserialize Quantity + raw_metadata = Group.deserialise_json(json_data["raw_metadata"]) verbose = json_data["verbose"] return SasData(name, data_contents, raw_metadata, verbose) diff --git a/sasdata/data_backing.py b/sasdata/data_backing.py index 0504963e..31521262 100644 --- a/sasdata/data_backing.py +++ b/sasdata/data_backing.py @@ -36,13 +36,25 @@ def summary(self, indent_amount: int = 0, indent: str = " ") -> str: return s + @staticmethod + def deserialise_json(json_data: dict) -> "Dataset": + name = json_data["name"] + data = "" # TODO: figure out QuantityType serialisation + attributes = {} + for key in json_data["attributes"]: + value = json_data["attributes"][key] + if isinstance(value, dict): + attributes[key] = Dataset.deserialise_json(value) + else: + attributes[key] = value + return Dataset(name, data, attributes) + def serialise_json(self): content = { - { - "name": self.name, - "data": "", # TODO: figure out QuantityType serialisation - "attributes": {} - } + "name": self.name, + "data": "", # TODO: figure out QuantityType serialisation + "attributes": {}, + "type": "dataset" } for key in self.attributes: value = self.attributes[key] @@ -64,14 +76,25 @@ def summary(self, indent_amount: int=0, indent=" "): return s + @staticmethod + def deserialise_json(json_data: dict) -> "Group": + name = json_data["name"] + children = {} + for key in json_data["children"]: + value = json_data["children"][key] + if value["type"] == "group": + children[key] = Group.deserialise_json(value) + else: + children[key] = Dataset.deserialise_json(value) + return Group(name, children) + def serialise_json(self): return { - { - "name": self.name, - "children": { - key: self.children[key].serialise_json() for key in self.children - } - } + "name": self.name, + "children": { + key: self.children[key].serialise_json() for key in self.children + }, + "type": "group" } class Function: From 78dcacd2932a7dce7db1d86e8fee76bd7c4c62e6 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 27 Feb 2025 15:24:42 -0500 Subject: [PATCH 109/129] Deserialization for Quantity classes --- sasdata/quantities/quantity.py | 40 +++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/sasdata/quantities/quantity.py b/sasdata/quantities/quantity.py index 749bd4e5..eba3acad 100644 --- a/sasdata/quantities/quantity.py +++ b/sasdata/quantities/quantity.py @@ -1085,6 +1085,13 @@ def summary(self): return s + @staticmethod + def deserialise_json(json_data: dict) -> "QuantityHistory": + # TODO: figure out if this should be deserialise_json + operation_tree = Operation.deserialise(json_data["operation_tree"]) + references = {} # TODO: figure out QuantityType + return QuantityHistory(operation_tree, references) + def serialise_json(self): return { "operation_tree": self.operation_tree.serialise(), @@ -1184,6 +1191,17 @@ def in_si_with_standard_error(self): else: return self.in_si(), None + @staticmethod + def deserialise_json(json_data: dict) -> "Quantity": + value = None + units = Unit.deserialise_json(json_data["units"]) + standard_error = None + hash_seed = json_data["hash_seed"] + history = QuantityHistory.deserialise_json(json_data["history"]) + quantity = Quantity(value, units, standard_error, hash_seed) + quantity.history = history + return quantity + # TODO: fill out actual values def serialise_json(self): return { @@ -1429,8 +1447,19 @@ def with_standard_error(self, standard_error: Quantity): raise UnitError(f"Standard error units ({standard_error.units}) " f"are not compatible with value units ({self.units})") + @staticmethod + def deserialise_json(json_data: dict) -> "NamedQuantity": + name = json_data["name"] + value = None + units = Unit.deserialise_json(json_data["units"]) + standard_error = None + history = QuantityHistory.deserialise_json(json_data["history"]) + quantity = NamedQuantity(name, value, units, standard_error) + quantity.history = history + return quantity + def serialise_json(self): - quantity = super()._serialise_json() + quantity = super().serialise_json() quantity["name"] = self.name return quantity @@ -1464,3 +1493,12 @@ def variance(self) -> Quantity: self._variance_cache = self.history.variance_propagate(self.units) return self._variance_cache + + + @staticmethod + def deserialise_json(json_data: dict) -> "DerivedQuantity": + value = None # TODO: figure out QuantityType + units = Unit.deserialise_json(json_data["units"]) + history = QuantityHistory.deserialise_json(json_data["history"]) + quantity = DerivedQuantity(value, units, history) + return quantity From c3629a655292af3446c2af1ce8feab1bbcf78d6d Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 27 Feb 2025 16:14:35 -0500 Subject: [PATCH 110/129] Add functionality to give and remove access to a file --- sasdata/fair_database/data/serializers.py | 5 +++++ sasdata/fair_database/data/urls.py | 1 + sasdata/fair_database/data/views.py | 14 +++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/serializers.py b/sasdata/fair_database/data/serializers.py index 8e9c2974..820561a7 100644 --- a/sasdata/fair_database/data/serializers.py +++ b/sasdata/fair_database/data/serializers.py @@ -14,6 +14,11 @@ def validate(self, data): return data +class AccessManagementSerializer(serializers.Serializer): + username = serializers.CharField(max_length=200, required=False) + access = serializers.BooleanField() + + class DataSetSerializer(serializers.ModelSerializer): class Meta: model = DataSet diff --git a/sasdata/fair_database/data/urls.py b/sasdata/fair_database/data/urls.py index 17fa2adf..615a3841 100644 --- a/sasdata/fair_database/data/urls.py +++ b/sasdata/fair_database/data/urls.py @@ -9,4 +9,5 @@ path("upload/", views.upload, name="upload data into db"), path("upload/<data_id>/", views.upload, name="update file in data"), path("<int:data_id>/download/", views.download, name="download data from db"), + path("manage/<int:data_id>", views.manage_access, name="manage access to files"), ] diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index ce278589..d96998a5 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -12,7 +12,7 @@ from rest_framework.response import Response from sasdata.dataloader.loader import Loader -from data.serializers import DataFileSerializer +from data.serializers import DataFileSerializer, AccessManagementSerializer from data.models import DataFile from data.forms import DataFileForm from fair_database import permissions @@ -118,6 +118,18 @@ def upload(request, data_id=None, version=None): return Response(return_data) +@api_view(["PUT"]) +def manage_access(request, data_id, version=None): + serializer = AccessManagementSerializer(request) + serializer.is_valid() + db = get_object_or_404(DataFile, id=data_id) + user = User.get_object_or_404(username=serializer.data["username"]) + if serializer.data["access"]: + db.users.add(user) + else: + db.users.remove(user) + + # downloads a file @api_view(["GET"]) def download(request, data_id, version=None): From de9ea17ff11fc29509b58a756b8d28b3e825f95a Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 27 Feb 2025 16:22:32 -0500 Subject: [PATCH 111/129] Add response to access management view --- sasdata/fair_database/data/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index d96998a5..e4dfc416 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -128,6 +128,12 @@ def manage_access(request, data_id, version=None): db.users.add(user) else: db.users.remove(user) + response_data = { + "user": user.username, + "file": db.pk, + "access": serializer.data["access"], + } + return Response(response_data) # downloads a file From 75567769c209d01d03c0a4cacfe89f0bd4d85019 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 27 Feb 2025 16:38:21 -0500 Subject: [PATCH 112/129] Tests for access granting/removal --- sasdata/fair_database/data/tests.py | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 10ebede8..007f2b54 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -196,3 +196,42 @@ def test_download_nonexistent(self): def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) + + +class TestAccessManagement(TestCase): + def setUp(self): + self.user1 = User.objects.create_user(username="testUser", password="secret") + self.user2 = User.objects.create_user(username="testUser2", password="secret2") + private_test_data = DataFile.objects.create( + id=1, current_user=self.user1, file_name="cyl_400_40.txt", is_public=False + ) + private_test_data.file.save( + "cyl_400_40.txt", open(find("cyl_400_40.txt"), "rb") + ) + shared_test_data = DataFile.objects.create( + id=2, current_user=self.user1, file_name="cyl_400_20.txt", is_public=False + ) + shared_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb")) + shared_test_data.users.add(self.user2) + self.client1 = APIClient() + self.client1.force_authenticate(self.user1) + self.client2 = APIClient() + self.client2.force_authenticate(self.user2) + + # test granting another user access to private data + def test_grant_access(self): + data = {"username": "testUser2", "access": True} + request1 = self.client1.put("/v1/data/manage/1/", data=data) + request2 = self.client2.get("/v1/data/load/1/") + self.assertEqual(request1.status_code, status.HTTP_200_OK) + self.assertEqual(request2.status_code, status.HTTP_200_OK) + + # test removing another user's access to private data + def test_remove_access(self): + data = {"username": "testUser2", "access": False} + request1 = self.client2.get("/v1/data/load/2/") + request2 = self.client1.put("/v1/data/manage/2/", data=data) + request3 = self.client2.get("/v1/data/load/2/") + self.assertEqual(request1.status_code, status.HTTP_200_OK) + self.assertEqual(request2.status_code, status.HTTP_200_OK) + self.assertEqual(request3.status_code, status.HTTP_403_FORBIDDEN) From 61f7779ef89d14cc3d912ec184a55ca64d797d6e Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 27 Feb 2025 16:38:54 -0500 Subject: [PATCH 113/129] Fix errors in access management view --- sasdata/fair_database/data/urls.py | 2 +- sasdata/fair_database/data/views.py | 4 +- .../media/uploaded_files/cyl_400_20.txt | 22 ++++++++ .../uploaded_files/cyl_400_20_Ig5qu3J.txt | 22 ++++++++ .../uploaded_files/cyl_400_20_Viyk7c7.txt | 22 ++++++++ .../uploaded_files/cyl_400_20_q7R0RfJ.txt | 22 ++++++++ .../media/uploaded_files/cyl_400_40.txt | 56 +++++++++++++++++++ .../uploaded_files/cyl_400_40_DmgpxJO.txt | 56 +++++++++++++++++++ .../uploaded_files/cyl_400_40_JvFpGUR.txt | 56 +++++++++++++++++++ .../uploaded_files/cyl_400_40_pF4ocC6.txt | 56 +++++++++++++++++++ .../uploaded_files/cyl_400_40_w6ynvDd.txt | 56 +++++++++++++++++++ 11 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_20.txt create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_20_Ig5qu3J.txt create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_20_Viyk7c7.txt create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_20_q7R0RfJ.txt create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40.txt create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40_DmgpxJO.txt create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40_JvFpGUR.txt create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40_pF4ocC6.txt create mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40_w6ynvDd.txt diff --git a/sasdata/fair_database/data/urls.py b/sasdata/fair_database/data/urls.py index 615a3841..8721927f 100644 --- a/sasdata/fair_database/data/urls.py +++ b/sasdata/fair_database/data/urls.py @@ -9,5 +9,5 @@ path("upload/", views.upload, name="upload data into db"), path("upload/<data_id>/", views.upload, name="update file in data"), path("<int:data_id>/download/", views.download, name="download data from db"), - path("manage/<int:data_id>", views.manage_access, name="manage access to files"), + path("manage/<int:data_id>/", views.manage_access, name="manage access to files"), ] diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index e4dfc416..629cc124 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -120,10 +120,10 @@ def upload(request, data_id=None, version=None): @api_view(["PUT"]) def manage_access(request, data_id, version=None): - serializer = AccessManagementSerializer(request) + serializer = AccessManagementSerializer(data=request.data) serializer.is_valid() db = get_object_or_404(DataFile, id=data_id) - user = User.get_object_or_404(username=serializer.data["username"]) + user = get_object_or_404(User, username=serializer.data["username"]) if serializer.data["access"]: db.users.add(user) else: diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_20.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_20.txt new file mode 100644 index 00000000..de714465 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_20.txt @@ -0,0 +1,22 @@ +<X> <Y> +0 -1.#IND +0.025 125.852 +0.05 53.6662 +0.075 26.0733 +0.1 11.8935 +0.125 4.61714 +0.15 1.29983 +0.175 0.171347 +0.2 0.0417614 +0.225 0.172719 +0.25 0.247876 +0.275 0.20301 +0.3 0.104599 +0.325 0.0285595 +0.35 0.00213344 +0.375 0.0137511 +0.4 0.0312374 +0.425 0.0350328 +0.45 0.0243172 +0.475 0.00923067 +0.5 0.00121297 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_20_Ig5qu3J.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_20_Ig5qu3J.txt new file mode 100644 index 00000000..de714465 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_20_Ig5qu3J.txt @@ -0,0 +1,22 @@ +<X> <Y> +0 -1.#IND +0.025 125.852 +0.05 53.6662 +0.075 26.0733 +0.1 11.8935 +0.125 4.61714 +0.15 1.29983 +0.175 0.171347 +0.2 0.0417614 +0.225 0.172719 +0.25 0.247876 +0.275 0.20301 +0.3 0.104599 +0.325 0.0285595 +0.35 0.00213344 +0.375 0.0137511 +0.4 0.0312374 +0.425 0.0350328 +0.45 0.0243172 +0.475 0.00923067 +0.5 0.00121297 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_20_Viyk7c7.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_20_Viyk7c7.txt new file mode 100644 index 00000000..de714465 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_20_Viyk7c7.txt @@ -0,0 +1,22 @@ +<X> <Y> +0 -1.#IND +0.025 125.852 +0.05 53.6662 +0.075 26.0733 +0.1 11.8935 +0.125 4.61714 +0.15 1.29983 +0.175 0.171347 +0.2 0.0417614 +0.225 0.172719 +0.25 0.247876 +0.275 0.20301 +0.3 0.104599 +0.325 0.0285595 +0.35 0.00213344 +0.375 0.0137511 +0.4 0.0312374 +0.425 0.0350328 +0.45 0.0243172 +0.475 0.00923067 +0.5 0.00121297 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_20_q7R0RfJ.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_20_q7R0RfJ.txt new file mode 100644 index 00000000..de714465 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_20_q7R0RfJ.txt @@ -0,0 +1,22 @@ +<X> <Y> +0 -1.#IND +0.025 125.852 +0.05 53.6662 +0.075 26.0733 +0.1 11.8935 +0.125 4.61714 +0.15 1.29983 +0.175 0.171347 +0.2 0.0417614 +0.225 0.172719 +0.25 0.247876 +0.275 0.20301 +0.3 0.104599 +0.325 0.0285595 +0.35 0.00213344 +0.375 0.0137511 +0.4 0.0312374 +0.425 0.0350328 +0.45 0.0243172 +0.475 0.00923067 +0.5 0.00121297 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40.txt new file mode 100644 index 00000000..b533fa18 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_40.txt @@ -0,0 +1,56 @@ +<X> <Y> +0 -1.#IND +0.00925926 1246.59 +0.0185185 612.143 +0.0277778 361.142 +0.037037 211.601 +0.0462963 122.127 +0.0555556 65.2385 +0.0648148 30.8914 +0.0740741 12.4737 +0.0833333 3.51371 +0.0925926 0.721835 +0.101852 0.583607 +0.111111 1.31084 +0.12037 1.9432 +0.12963 1.94286 +0.138889 1.58912 +0.148148 0.987076 +0.157407 0.456678 +0.166667 0.147595 +0.175926 0.027441 +0.185185 0.0999575 +0.194444 0.198717 +0.203704 0.277667 +0.212963 0.288172 +0.222222 0.220056 +0.231481 0.139378 +0.240741 0.0541106 +0.25 0.0140158 +0.259259 0.0132187 +0.268519 0.0336301 +0.277778 0.0672911 +0.287037 0.0788983 +0.296296 0.0764438 +0.305556 0.0555445 +0.314815 0.0280548 +0.324074 0.0111798 +0.333333 0.00156156 +0.342593 0.00830883 +0.351852 0.0186266 +0.361111 0.0275426 +0.37037 0.03192 +0.37963 0.0255329 +0.388889 0.0175216 +0.398148 0.0073075 +0.407407 0.0016631 +0.416667 0.00224153 +0.425926 0.0051335 +0.435185 0.0112914 +0.444444 0.0138209 +0.453704 0.0137453 +0.462963 0.0106682 +0.472222 0.00532472 +0.481481 0.00230646 +0.490741 0.000335344 +0.5 0.00177224 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40_DmgpxJO.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40_DmgpxJO.txt new file mode 100644 index 00000000..b533fa18 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_40_DmgpxJO.txt @@ -0,0 +1,56 @@ +<X> <Y> +0 -1.#IND +0.00925926 1246.59 +0.0185185 612.143 +0.0277778 361.142 +0.037037 211.601 +0.0462963 122.127 +0.0555556 65.2385 +0.0648148 30.8914 +0.0740741 12.4737 +0.0833333 3.51371 +0.0925926 0.721835 +0.101852 0.583607 +0.111111 1.31084 +0.12037 1.9432 +0.12963 1.94286 +0.138889 1.58912 +0.148148 0.987076 +0.157407 0.456678 +0.166667 0.147595 +0.175926 0.027441 +0.185185 0.0999575 +0.194444 0.198717 +0.203704 0.277667 +0.212963 0.288172 +0.222222 0.220056 +0.231481 0.139378 +0.240741 0.0541106 +0.25 0.0140158 +0.259259 0.0132187 +0.268519 0.0336301 +0.277778 0.0672911 +0.287037 0.0788983 +0.296296 0.0764438 +0.305556 0.0555445 +0.314815 0.0280548 +0.324074 0.0111798 +0.333333 0.00156156 +0.342593 0.00830883 +0.351852 0.0186266 +0.361111 0.0275426 +0.37037 0.03192 +0.37963 0.0255329 +0.388889 0.0175216 +0.398148 0.0073075 +0.407407 0.0016631 +0.416667 0.00224153 +0.425926 0.0051335 +0.435185 0.0112914 +0.444444 0.0138209 +0.453704 0.0137453 +0.462963 0.0106682 +0.472222 0.00532472 +0.481481 0.00230646 +0.490741 0.000335344 +0.5 0.00177224 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40_JvFpGUR.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40_JvFpGUR.txt new file mode 100644 index 00000000..b533fa18 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_40_JvFpGUR.txt @@ -0,0 +1,56 @@ +<X> <Y> +0 -1.#IND +0.00925926 1246.59 +0.0185185 612.143 +0.0277778 361.142 +0.037037 211.601 +0.0462963 122.127 +0.0555556 65.2385 +0.0648148 30.8914 +0.0740741 12.4737 +0.0833333 3.51371 +0.0925926 0.721835 +0.101852 0.583607 +0.111111 1.31084 +0.12037 1.9432 +0.12963 1.94286 +0.138889 1.58912 +0.148148 0.987076 +0.157407 0.456678 +0.166667 0.147595 +0.175926 0.027441 +0.185185 0.0999575 +0.194444 0.198717 +0.203704 0.277667 +0.212963 0.288172 +0.222222 0.220056 +0.231481 0.139378 +0.240741 0.0541106 +0.25 0.0140158 +0.259259 0.0132187 +0.268519 0.0336301 +0.277778 0.0672911 +0.287037 0.0788983 +0.296296 0.0764438 +0.305556 0.0555445 +0.314815 0.0280548 +0.324074 0.0111798 +0.333333 0.00156156 +0.342593 0.00830883 +0.351852 0.0186266 +0.361111 0.0275426 +0.37037 0.03192 +0.37963 0.0255329 +0.388889 0.0175216 +0.398148 0.0073075 +0.407407 0.0016631 +0.416667 0.00224153 +0.425926 0.0051335 +0.435185 0.0112914 +0.444444 0.0138209 +0.453704 0.0137453 +0.462963 0.0106682 +0.472222 0.00532472 +0.481481 0.00230646 +0.490741 0.000335344 +0.5 0.00177224 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40_pF4ocC6.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40_pF4ocC6.txt new file mode 100644 index 00000000..b533fa18 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_40_pF4ocC6.txt @@ -0,0 +1,56 @@ +<X> <Y> +0 -1.#IND +0.00925926 1246.59 +0.0185185 612.143 +0.0277778 361.142 +0.037037 211.601 +0.0462963 122.127 +0.0555556 65.2385 +0.0648148 30.8914 +0.0740741 12.4737 +0.0833333 3.51371 +0.0925926 0.721835 +0.101852 0.583607 +0.111111 1.31084 +0.12037 1.9432 +0.12963 1.94286 +0.138889 1.58912 +0.148148 0.987076 +0.157407 0.456678 +0.166667 0.147595 +0.175926 0.027441 +0.185185 0.0999575 +0.194444 0.198717 +0.203704 0.277667 +0.212963 0.288172 +0.222222 0.220056 +0.231481 0.139378 +0.240741 0.0541106 +0.25 0.0140158 +0.259259 0.0132187 +0.268519 0.0336301 +0.277778 0.0672911 +0.287037 0.0788983 +0.296296 0.0764438 +0.305556 0.0555445 +0.314815 0.0280548 +0.324074 0.0111798 +0.333333 0.00156156 +0.342593 0.00830883 +0.351852 0.0186266 +0.361111 0.0275426 +0.37037 0.03192 +0.37963 0.0255329 +0.388889 0.0175216 +0.398148 0.0073075 +0.407407 0.0016631 +0.416667 0.00224153 +0.425926 0.0051335 +0.435185 0.0112914 +0.444444 0.0138209 +0.453704 0.0137453 +0.462963 0.0106682 +0.472222 0.00532472 +0.481481 0.00230646 +0.490741 0.000335344 +0.5 0.00177224 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40_w6ynvDd.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40_w6ynvDd.txt new file mode 100644 index 00000000..b533fa18 --- /dev/null +++ b/sasdata/fair_database/media/uploaded_files/cyl_400_40_w6ynvDd.txt @@ -0,0 +1,56 @@ +<X> <Y> +0 -1.#IND +0.00925926 1246.59 +0.0185185 612.143 +0.0277778 361.142 +0.037037 211.601 +0.0462963 122.127 +0.0555556 65.2385 +0.0648148 30.8914 +0.0740741 12.4737 +0.0833333 3.51371 +0.0925926 0.721835 +0.101852 0.583607 +0.111111 1.31084 +0.12037 1.9432 +0.12963 1.94286 +0.138889 1.58912 +0.148148 0.987076 +0.157407 0.456678 +0.166667 0.147595 +0.175926 0.027441 +0.185185 0.0999575 +0.194444 0.198717 +0.203704 0.277667 +0.212963 0.288172 +0.222222 0.220056 +0.231481 0.139378 +0.240741 0.0541106 +0.25 0.0140158 +0.259259 0.0132187 +0.268519 0.0336301 +0.277778 0.0672911 +0.287037 0.0788983 +0.296296 0.0764438 +0.305556 0.0555445 +0.314815 0.0280548 +0.324074 0.0111798 +0.333333 0.00156156 +0.342593 0.00830883 +0.351852 0.0186266 +0.361111 0.0275426 +0.37037 0.03192 +0.37963 0.0255329 +0.388889 0.0175216 +0.398148 0.0073075 +0.407407 0.0016631 +0.416667 0.00224153 +0.425926 0.0051335 +0.435185 0.0112914 +0.444444 0.0138209 +0.453704 0.0137453 +0.462963 0.0106682 +0.472222 0.00532472 +0.481481 0.00230646 +0.490741 0.000335344 +0.5 0.00177224 From 8593ff99699c5fa2188c33e1305ce96294c40cbc Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Fri, 28 Feb 2025 12:00:09 -0500 Subject: [PATCH 114/129] More tests for access management --- sasdata/fair_database/data/tests.py | 46 +++++++++++++-- .../media/uploaded_files/cyl_400_20.txt | 22 -------- .../uploaded_files/cyl_400_20_Ig5qu3J.txt | 22 -------- .../uploaded_files/cyl_400_20_Viyk7c7.txt | 22 -------- .../uploaded_files/cyl_400_20_q7R0RfJ.txt | 22 -------- .../media/uploaded_files/cyl_400_40.txt | 56 ------------------- .../uploaded_files/cyl_400_40_DmgpxJO.txt | 56 ------------------- .../uploaded_files/cyl_400_40_JvFpGUR.txt | 56 ------------------- .../uploaded_files/cyl_400_40_pF4ocC6.txt | 56 ------------------- .../uploaded_files/cyl_400_40_w6ynvDd.txt | 56 ------------------- 10 files changed, 41 insertions(+), 373 deletions(-) delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_20.txt delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_20_Ig5qu3J.txt delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_20_Viyk7c7.txt delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_20_q7R0RfJ.txt delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40.txt delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40_DmgpxJO.txt delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40_JvFpGUR.txt delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40_pF4ocC6.txt delete mode 100644 sasdata/fair_database/media/uploaded_files/cyl_400_40_w6ynvDd.txt diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 007f2b54..368eb55e 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -202,17 +202,19 @@ class TestAccessManagement(TestCase): def setUp(self): self.user1 = User.objects.create_user(username="testUser", password="secret") self.user2 = User.objects.create_user(username="testUser2", password="secret2") - private_test_data = DataFile.objects.create( + self.private_test_data = DataFile.objects.create( id=1, current_user=self.user1, file_name="cyl_400_40.txt", is_public=False ) - private_test_data.file.save( + self.private_test_data.file.save( "cyl_400_40.txt", open(find("cyl_400_40.txt"), "rb") ) - shared_test_data = DataFile.objects.create( + self.shared_test_data = DataFile.objects.create( id=2, current_user=self.user1, file_name="cyl_400_20.txt", is_public=False ) - shared_test_data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb")) - shared_test_data.users.add(self.user2) + self.shared_test_data.file.save( + "cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb") + ) + self.shared_test_data.users.add(self.user2) self.client1 = APIClient() self.client1.force_authenticate(self.user1) self.client2 = APIClient() @@ -235,3 +237,37 @@ def test_remove_access(self): self.assertEqual(request1.status_code, status.HTTP_200_OK) self.assertEqual(request2.status_code, status.HTTP_200_OK) self.assertEqual(request3.status_code, status.HTTP_403_FORBIDDEN) + + def test_remove_no_access(self): + data = {"username": "testUser2", "access": False} + request1 = self.client2.get("/v1/data/load/1/") + request2 = self.client1.put("/v1/data/manage/1/", data=data) + request3 = self.client2.get("/v1/data/load/1/") + self.assertEqual(request1.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(request2.status_code, status.HTTP_200_OK) + self.assertEqual(request3.status_code, status.HTTP_403_FORBIDDEN) + + def test_cant_revoke_own_access(self): + data = {"username": "testUser", "access": False} + request1 = self.client1.put("/v1/data/manage/1/", data=data) + request2 = self.client1.get("/v1/data/load/1/") + self.assertEqual(request1.status_code, status.HTTP_200_OK) + self.assertEqual(request2.status_code, status.HTTP_200_OK) + + def test_grant_existing_access(self): + data = {"username": "testUser2", "access": True} + request1 = self.client2.get("/v1/data/load/2/") + request2 = self.client1.put("/v1/data/manage/2/", data=data) + request3 = self.client2.get("/v1/data/load/2/") + self.assertEqual(request1.status_code, status.HTTP_200_OK) + self.assertEqual(request2.status_code, status.HTTP_200_OK) + self.assertEqual(request3.status_code, status.HTTP_200_OK) + + def test_no_edit_access(self): + data = {"is_public": True} + request = self.client2.put("/v1/data/upload/2/", data=data) + self.assertEqual(request.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(self.shared_test_data.is_public) + + def tearDown(self): + shutil.rmtree(settings.MEDIA_ROOT) diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_20.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_20.txt deleted file mode 100644 index de714465..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_20.txt +++ /dev/null @@ -1,22 +0,0 @@ -<X> <Y> -0 -1.#IND -0.025 125.852 -0.05 53.6662 -0.075 26.0733 -0.1 11.8935 -0.125 4.61714 -0.15 1.29983 -0.175 0.171347 -0.2 0.0417614 -0.225 0.172719 -0.25 0.247876 -0.275 0.20301 -0.3 0.104599 -0.325 0.0285595 -0.35 0.00213344 -0.375 0.0137511 -0.4 0.0312374 -0.425 0.0350328 -0.45 0.0243172 -0.475 0.00923067 -0.5 0.00121297 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_20_Ig5qu3J.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_20_Ig5qu3J.txt deleted file mode 100644 index de714465..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_20_Ig5qu3J.txt +++ /dev/null @@ -1,22 +0,0 @@ -<X> <Y> -0 -1.#IND -0.025 125.852 -0.05 53.6662 -0.075 26.0733 -0.1 11.8935 -0.125 4.61714 -0.15 1.29983 -0.175 0.171347 -0.2 0.0417614 -0.225 0.172719 -0.25 0.247876 -0.275 0.20301 -0.3 0.104599 -0.325 0.0285595 -0.35 0.00213344 -0.375 0.0137511 -0.4 0.0312374 -0.425 0.0350328 -0.45 0.0243172 -0.475 0.00923067 -0.5 0.00121297 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_20_Viyk7c7.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_20_Viyk7c7.txt deleted file mode 100644 index de714465..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_20_Viyk7c7.txt +++ /dev/null @@ -1,22 +0,0 @@ -<X> <Y> -0 -1.#IND -0.025 125.852 -0.05 53.6662 -0.075 26.0733 -0.1 11.8935 -0.125 4.61714 -0.15 1.29983 -0.175 0.171347 -0.2 0.0417614 -0.225 0.172719 -0.25 0.247876 -0.275 0.20301 -0.3 0.104599 -0.325 0.0285595 -0.35 0.00213344 -0.375 0.0137511 -0.4 0.0312374 -0.425 0.0350328 -0.45 0.0243172 -0.475 0.00923067 -0.5 0.00121297 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_20_q7R0RfJ.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_20_q7R0RfJ.txt deleted file mode 100644 index de714465..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_20_q7R0RfJ.txt +++ /dev/null @@ -1,22 +0,0 @@ -<X> <Y> -0 -1.#IND -0.025 125.852 -0.05 53.6662 -0.075 26.0733 -0.1 11.8935 -0.125 4.61714 -0.15 1.29983 -0.175 0.171347 -0.2 0.0417614 -0.225 0.172719 -0.25 0.247876 -0.275 0.20301 -0.3 0.104599 -0.325 0.0285595 -0.35 0.00213344 -0.375 0.0137511 -0.4 0.0312374 -0.425 0.0350328 -0.45 0.0243172 -0.475 0.00923067 -0.5 0.00121297 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40.txt deleted file mode 100644 index b533fa18..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_40.txt +++ /dev/null @@ -1,56 +0,0 @@ -<X> <Y> -0 -1.#IND -0.00925926 1246.59 -0.0185185 612.143 -0.0277778 361.142 -0.037037 211.601 -0.0462963 122.127 -0.0555556 65.2385 -0.0648148 30.8914 -0.0740741 12.4737 -0.0833333 3.51371 -0.0925926 0.721835 -0.101852 0.583607 -0.111111 1.31084 -0.12037 1.9432 -0.12963 1.94286 -0.138889 1.58912 -0.148148 0.987076 -0.157407 0.456678 -0.166667 0.147595 -0.175926 0.027441 -0.185185 0.0999575 -0.194444 0.198717 -0.203704 0.277667 -0.212963 0.288172 -0.222222 0.220056 -0.231481 0.139378 -0.240741 0.0541106 -0.25 0.0140158 -0.259259 0.0132187 -0.268519 0.0336301 -0.277778 0.0672911 -0.287037 0.0788983 -0.296296 0.0764438 -0.305556 0.0555445 -0.314815 0.0280548 -0.324074 0.0111798 -0.333333 0.00156156 -0.342593 0.00830883 -0.351852 0.0186266 -0.361111 0.0275426 -0.37037 0.03192 -0.37963 0.0255329 -0.388889 0.0175216 -0.398148 0.0073075 -0.407407 0.0016631 -0.416667 0.00224153 -0.425926 0.0051335 -0.435185 0.0112914 -0.444444 0.0138209 -0.453704 0.0137453 -0.462963 0.0106682 -0.472222 0.00532472 -0.481481 0.00230646 -0.490741 0.000335344 -0.5 0.00177224 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40_DmgpxJO.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40_DmgpxJO.txt deleted file mode 100644 index b533fa18..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_40_DmgpxJO.txt +++ /dev/null @@ -1,56 +0,0 @@ -<X> <Y> -0 -1.#IND -0.00925926 1246.59 -0.0185185 612.143 -0.0277778 361.142 -0.037037 211.601 -0.0462963 122.127 -0.0555556 65.2385 -0.0648148 30.8914 -0.0740741 12.4737 -0.0833333 3.51371 -0.0925926 0.721835 -0.101852 0.583607 -0.111111 1.31084 -0.12037 1.9432 -0.12963 1.94286 -0.138889 1.58912 -0.148148 0.987076 -0.157407 0.456678 -0.166667 0.147595 -0.175926 0.027441 -0.185185 0.0999575 -0.194444 0.198717 -0.203704 0.277667 -0.212963 0.288172 -0.222222 0.220056 -0.231481 0.139378 -0.240741 0.0541106 -0.25 0.0140158 -0.259259 0.0132187 -0.268519 0.0336301 -0.277778 0.0672911 -0.287037 0.0788983 -0.296296 0.0764438 -0.305556 0.0555445 -0.314815 0.0280548 -0.324074 0.0111798 -0.333333 0.00156156 -0.342593 0.00830883 -0.351852 0.0186266 -0.361111 0.0275426 -0.37037 0.03192 -0.37963 0.0255329 -0.388889 0.0175216 -0.398148 0.0073075 -0.407407 0.0016631 -0.416667 0.00224153 -0.425926 0.0051335 -0.435185 0.0112914 -0.444444 0.0138209 -0.453704 0.0137453 -0.462963 0.0106682 -0.472222 0.00532472 -0.481481 0.00230646 -0.490741 0.000335344 -0.5 0.00177224 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40_JvFpGUR.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40_JvFpGUR.txt deleted file mode 100644 index b533fa18..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_40_JvFpGUR.txt +++ /dev/null @@ -1,56 +0,0 @@ -<X> <Y> -0 -1.#IND -0.00925926 1246.59 -0.0185185 612.143 -0.0277778 361.142 -0.037037 211.601 -0.0462963 122.127 -0.0555556 65.2385 -0.0648148 30.8914 -0.0740741 12.4737 -0.0833333 3.51371 -0.0925926 0.721835 -0.101852 0.583607 -0.111111 1.31084 -0.12037 1.9432 -0.12963 1.94286 -0.138889 1.58912 -0.148148 0.987076 -0.157407 0.456678 -0.166667 0.147595 -0.175926 0.027441 -0.185185 0.0999575 -0.194444 0.198717 -0.203704 0.277667 -0.212963 0.288172 -0.222222 0.220056 -0.231481 0.139378 -0.240741 0.0541106 -0.25 0.0140158 -0.259259 0.0132187 -0.268519 0.0336301 -0.277778 0.0672911 -0.287037 0.0788983 -0.296296 0.0764438 -0.305556 0.0555445 -0.314815 0.0280548 -0.324074 0.0111798 -0.333333 0.00156156 -0.342593 0.00830883 -0.351852 0.0186266 -0.361111 0.0275426 -0.37037 0.03192 -0.37963 0.0255329 -0.388889 0.0175216 -0.398148 0.0073075 -0.407407 0.0016631 -0.416667 0.00224153 -0.425926 0.0051335 -0.435185 0.0112914 -0.444444 0.0138209 -0.453704 0.0137453 -0.462963 0.0106682 -0.472222 0.00532472 -0.481481 0.00230646 -0.490741 0.000335344 -0.5 0.00177224 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40_pF4ocC6.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40_pF4ocC6.txt deleted file mode 100644 index b533fa18..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_40_pF4ocC6.txt +++ /dev/null @@ -1,56 +0,0 @@ -<X> <Y> -0 -1.#IND -0.00925926 1246.59 -0.0185185 612.143 -0.0277778 361.142 -0.037037 211.601 -0.0462963 122.127 -0.0555556 65.2385 -0.0648148 30.8914 -0.0740741 12.4737 -0.0833333 3.51371 -0.0925926 0.721835 -0.101852 0.583607 -0.111111 1.31084 -0.12037 1.9432 -0.12963 1.94286 -0.138889 1.58912 -0.148148 0.987076 -0.157407 0.456678 -0.166667 0.147595 -0.175926 0.027441 -0.185185 0.0999575 -0.194444 0.198717 -0.203704 0.277667 -0.212963 0.288172 -0.222222 0.220056 -0.231481 0.139378 -0.240741 0.0541106 -0.25 0.0140158 -0.259259 0.0132187 -0.268519 0.0336301 -0.277778 0.0672911 -0.287037 0.0788983 -0.296296 0.0764438 -0.305556 0.0555445 -0.314815 0.0280548 -0.324074 0.0111798 -0.333333 0.00156156 -0.342593 0.00830883 -0.351852 0.0186266 -0.361111 0.0275426 -0.37037 0.03192 -0.37963 0.0255329 -0.388889 0.0175216 -0.398148 0.0073075 -0.407407 0.0016631 -0.416667 0.00224153 -0.425926 0.0051335 -0.435185 0.0112914 -0.444444 0.0138209 -0.453704 0.0137453 -0.462963 0.0106682 -0.472222 0.00532472 -0.481481 0.00230646 -0.490741 0.000335344 -0.5 0.00177224 diff --git a/sasdata/fair_database/media/uploaded_files/cyl_400_40_w6ynvDd.txt b/sasdata/fair_database/media/uploaded_files/cyl_400_40_w6ynvDd.txt deleted file mode 100644 index b533fa18..00000000 --- a/sasdata/fair_database/media/uploaded_files/cyl_400_40_w6ynvDd.txt +++ /dev/null @@ -1,56 +0,0 @@ -<X> <Y> -0 -1.#IND -0.00925926 1246.59 -0.0185185 612.143 -0.0277778 361.142 -0.037037 211.601 -0.0462963 122.127 -0.0555556 65.2385 -0.0648148 30.8914 -0.0740741 12.4737 -0.0833333 3.51371 -0.0925926 0.721835 -0.101852 0.583607 -0.111111 1.31084 -0.12037 1.9432 -0.12963 1.94286 -0.138889 1.58912 -0.148148 0.987076 -0.157407 0.456678 -0.166667 0.147595 -0.175926 0.027441 -0.185185 0.0999575 -0.194444 0.198717 -0.203704 0.277667 -0.212963 0.288172 -0.222222 0.220056 -0.231481 0.139378 -0.240741 0.0541106 -0.25 0.0140158 -0.259259 0.0132187 -0.268519 0.0336301 -0.277778 0.0672911 -0.287037 0.0788983 -0.296296 0.0764438 -0.305556 0.0555445 -0.314815 0.0280548 -0.324074 0.0111798 -0.333333 0.00156156 -0.342593 0.00830883 -0.351852 0.0186266 -0.361111 0.0275426 -0.37037 0.03192 -0.37963 0.0255329 -0.388889 0.0175216 -0.398148 0.0073075 -0.407407 0.0016631 -0.416667 0.00224153 -0.425926 0.0051335 -0.435185 0.0112914 -0.444444 0.0138209 -0.453704 0.0137453 -0.462963 0.0106682 -0.472222 0.00532472 -0.481481 0.00230646 -0.490741 0.000335344 -0.5 0.00177224 From 16b0dc15142e4c67b3817bd38017a81329a41dbd Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Mar 2025 11:15:39 -0500 Subject: [PATCH 115/129] Remove redundant code in permissions --- sasdata/fair_database/fair_database/permissions.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/sasdata/fair_database/fair_database/permissions.py b/sasdata/fair_database/fair_database/permissions.py index 224e4c00..89e677be 100644 --- a/sasdata/fair_database/fair_database/permissions.py +++ b/sasdata/fair_database/fair_database/permissions.py @@ -26,11 +26,4 @@ def has_object_permission(self, request, view, obj): def check_permissions(request, obj): - if request.method == "GET": - if obj.is_public or has_access(request, obj): - return True - elif request.method == "DELETE": - if obj.is_private and is_owner(request, obj): - return True - else: - return is_owner(request, obj) + return DataPermission().has_object_permission(request, None, obj) From 92125d6916f4d64a2fcded0ed3cc313506d313c5 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Mar 2025 15:37:32 -0500 Subject: [PATCH 116/129] QuantityType serialisation for some types --- sasdata/data.py | 1 - sasdata/quantities/quantity.py | 25 +++++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/sasdata/data.py b/sasdata/data.py index 3961bdd5..331cede6 100644 --- a/sasdata/data.py +++ b/sasdata/data.py @@ -61,7 +61,6 @@ def deserialise_json(json_data: dict) -> "SasData": def serialise(self) -> str: return json.dumps(self._serialise_json()) - # TODO: replace with serialization methods when written def _serialise_json(self) -> dict[str, Any]: return { "name": self.name, diff --git a/sasdata/quantities/quantity.py b/sasdata/quantities/quantity.py index eba3acad..e6d145bb 100644 --- a/sasdata/quantities/quantity.py +++ b/sasdata/quantities/quantity.py @@ -1,6 +1,7 @@ from typing import Self import numpy as np +from docutils.frontend import validate_ternary from numpy._typing import ArrayLike from sasdata.quantities import units @@ -995,6 +996,12 @@ def hash_data_via_numpy(*data: ArrayLike): QuantityType = TypeVar("QuantityType") +# TODO: figure out how to handle np.ndarray serialization (save as file or otherwise) +def quantity_type_serialisation(var): + if isinstance(var, (str, int, float)): + return var + return None + class QuantityHistory: """ Class that holds the information for keeping track of operations done on quantities """ @@ -1087,9 +1094,11 @@ def summary(self): @staticmethod def deserialise_json(json_data: dict) -> "QuantityHistory": - # TODO: figure out if this should be deserialise_json operation_tree = Operation.deserialise(json_data["operation_tree"]) - references = {} # TODO: figure out QuantityType + references = { + key: Quantity.deserialise_json(json_data["references"][key]) + for key in json_data["references"] + } return QuantityHistory(operation_tree, references) def serialise_json(self): @@ -1193,9 +1202,9 @@ def in_si_with_standard_error(self): @staticmethod def deserialise_json(json_data: dict) -> "Quantity": - value = None + value = None # TODO QuantityType deserialisation units = Unit.deserialise_json(json_data["units"]) - standard_error = None + standard_error = None #TODO QuantityType deserialisation hash_seed = json_data["hash_seed"] history = QuantityHistory.deserialise_json(json_data["history"]) quantity = Quantity(value, units, standard_error, hash_seed) @@ -1205,9 +1214,9 @@ def deserialise_json(json_data: dict) -> "Quantity": # TODO: fill out actual values def serialise_json(self): return { - "value": "", # figure out QuantityType serialisation + "value": quantity_type_serialisation(self.value), "units": self.units.serialise_json(), # Unit serialisation - "standard_error": "", # also QuantityType serialisation + "standard_error": quantity_type_serialisation(self._variance ** 0.5), "hash_seed": self._hash_seed, # is this just a string? "history": self.history.serialise_json() } @@ -1450,9 +1459,9 @@ def with_standard_error(self, standard_error: Quantity): @staticmethod def deserialise_json(json_data: dict) -> "NamedQuantity": name = json_data["name"] - value = None + value = None # TODO: figure out QuantityType deserialization units = Unit.deserialise_json(json_data["units"]) - standard_error = None + standard_error = None # TODO: QuantityType deserialization history = QuantityHistory.deserialise_json(json_data["history"]) quantity = NamedQuantity(name, value, units, standard_error) quantity.history = history From 2db3c3876cdb16c18e04c875eefc46e0bff43404 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Mar 2025 15:39:09 -0500 Subject: [PATCH 117/129] Allow viewing who has access to a file --- sasdata/fair_database/data/views.py | 41 +++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 629cc124..8069b0a3 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -118,22 +118,35 @@ def upload(request, data_id=None, version=None): return Response(return_data) -@api_view(["PUT"]) +# view or control who has access to a file +@api_view(["GET", "PUT"]) def manage_access(request, data_id, version=None): - serializer = AccessManagementSerializer(data=request.data) - serializer.is_valid() db = get_object_or_404(DataFile, id=data_id) - user = get_object_or_404(User, username=serializer.data["username"]) - if serializer.data["access"]: - db.users.add(user) - else: - db.users.remove(user) - response_data = { - "user": user.username, - "file": db.pk, - "access": serializer.data["access"], - } - return Response(response_data) + if not permissions.is_owner(request, db): + return HttpResponseForbidden("Must be the data owner to manage access") + if request.method == "GET": + response_data = { + "file": db.pk, + "file_name": db.file_name, + "users": [user.username for user in db.users], + } + return Response(response_data) + elif request.method == "PUT": + serializer = AccessManagementSerializer(data=request.data) + serializer.is_valid() + user = get_object_or_404(User, username=serializer.data["username"]) + if serializer.data["access"]: + db.users.add(user) + else: + db.users.remove(user) + response_data = { + "user": user.username, + "file": db.pk, + "file_name": db.file_name, + "access": serializer.data["access"], + } + return Response(response_data) + return HttpResponseBadRequest() # downloads a file From 9980486a8b924dc85705e5ed0b9925dbe6615d04 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Mar 2025 15:51:29 -0500 Subject: [PATCH 118/129] Test listing users with access to a file --- sasdata/fair_database/data/tests.py | 14 ++++++++++++++ sasdata/fair_database/data/views.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 368eb55e..2df0b35e 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -220,6 +220,20 @@ def setUp(self): self.client2 = APIClient() self.client2.force_authenticate(self.user2) + # test viewing no one with access + def test_view_no_access(self): + request = self.client1.get("/v1/data/manage/1/") + data = {"file": 1, "file_name": "cyl_400_40.txt", "users": []} + self.assertEqual(request.status_code, status.HTTP_200_OK) + self.assertEqual(request.data, data) + + # test viewing list of users with access + def test_view_access(self): + request = self.client1.get("/v1/data/manage/2/") + data = {"file": 2, "file_name": "cyl_400_20.txt", "users": ["testUser2"]} + self.assertEqual(request.status_code, status.HTTP_200_OK) + self.assertEqual(request.data, data) + # test granting another user access to private data def test_grant_access(self): data = {"username": "testUser2", "access": True} diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 8069b0a3..8bf48882 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -128,7 +128,7 @@ def manage_access(request, data_id, version=None): response_data = { "file": db.pk, "file_name": db.file_name, - "users": [user.username for user in db.users], + "users": [user.username for user in db.users.all()], } return Response(response_data) elif request.method == "PUT": From 273805cc6dd9c8ec7271a588006a8e734d1e8d63 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Mon, 3 Mar 2025 16:00:47 -0500 Subject: [PATCH 119/129] Test permissions on file access management views --- sasdata/fair_database/data/tests.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 2df0b35e..df15deaa 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -283,5 +283,23 @@ def test_no_edit_access(self): self.assertEqual(request.status_code, status.HTTP_403_FORBIDDEN) self.assertFalse(self.shared_test_data.is_public) + def test_only_view_access_to_owned_file(self): + request1 = self.client2.get("/v1/data/manage/1/") + request2 = self.client2.get("/v1/data/manage/2/") + self.assertEqual(request1.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) + + def test_only_edit_access_to_owned_file(self): + data1 = {"username": "testUser2", "access": True} + data2 = {"username": "testUser1", "access": False} + request1 = self.client2.put("/v1/data/manage/1/", data=data1) + request2 = self.client2.put("/v1/data/manage/2/", data=data2) + request3 = self.client2.get("/v1/data/load/1/") + request4 = self.client1.get("/v1/data/load/2/") + self.assertEqual(request1.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(request3.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(request4.status_code, status.HTTP_200_OK) + def tearDown(self): shutil.rmtree(settings.MEDIA_ROOT) From 8cbad2198912841b8af48e9f51482681ddc48673 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Mar 2025 10:35:48 -0500 Subject: [PATCH 120/129] Serialize variance not standard deviation --- sasdata/quantities/quantity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sasdata/quantities/quantity.py b/sasdata/quantities/quantity.py index e6d145bb..ae523d51 100644 --- a/sasdata/quantities/quantity.py +++ b/sasdata/quantities/quantity.py @@ -1216,7 +1216,7 @@ def serialise_json(self): return { "value": quantity_type_serialisation(self.value), "units": self.units.serialise_json(), # Unit serialisation - "standard_error": quantity_type_serialisation(self._variance ** 0.5), + "variance": quantity_type_serialisation(self._variance), "hash_seed": self._hash_seed, # is this just a string? "history": self.history.serialise_json() } From 91be6c610a7ef0582e3668c47def8905f45496ae Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Mar 2025 11:01:58 -0500 Subject: [PATCH 121/129] Finish models pending later changes --- ...definition_metadata_instrument_and_more.py | 52 +++++++++++++++++++ sasdata/fair_database/data/models.py | 51 +++++++++++++----- 2 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 sasdata/fair_database/data/migrations/0009_metadata_definition_metadata_instrument_and_more.py diff --git a/sasdata/fair_database/data/migrations/0009_metadata_definition_metadata_instrument_and_more.py b/sasdata/fair_database/data/migrations/0009_metadata_definition_metadata_instrument_and_more.py new file mode 100644 index 00000000..e6ae8a4a --- /dev/null +++ b/sasdata/fair_database/data/migrations/0009_metadata_definition_metadata_instrument_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 5.1.6 on 2025-03-05 15:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("data", "0008_alter_datafile_users_alter_dataset_users"), + ] + + operations = [ + migrations.AddField( + model_name="metadata", + name="definition", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="metadata", + name="instrument", + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name="metadata", + name="process", + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name="metadata", + name="raw_metadata", + field=models.JSONField(default=dict), + ), + migrations.AddField( + model_name="metadata", + name="run", + field=models.CharField(blank=True, max_length=500, null=True), + ), + migrations.AddField( + model_name="metadata", + name="sample", + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name="metadata", + name="title", + field=models.CharField(default="Title", max_length=500), + ), + migrations.AddField( + model_name="metadata", + name="transmission_spectrum", + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/sasdata/fair_database/data/models.py b/sasdata/fair_database/data/models.py index 2ad7b664..797ba756 100644 --- a/sasdata/fair_database/data/models.py +++ b/sasdata/fair_database/data/models.py @@ -44,27 +44,26 @@ class DataFile(Data): class DataSet(Data): """Database model for a set of data and associated metadata.""" + # TODO: Update when plan for this is finished. + # dataset name name = models.CharField(max_length=200) # associated files files = models.ManyToManyField(DataFile) - # metadata + # metadata - maybe a foreign key? metadata = models.OneToOneField("MetaData", on_delete=models.CASCADE) # ordinate - # ordinate = models.JSONField() + # ordinate = models.ForeignKey("Quantity", on_delete=models.CASCADE) # abscissae - # abscissae = models.JSONField() + # abscissae = models.ManyToManyField("Quantity", on_delete=models.CASCADE) - # data contents + # data contents - maybe ManyToManyField # data_contents = models.JSONField() - # metadata - # raw_metadata = models.JSONField() - class Quantity(models.Model): """Database model for data quantities such as the ordinate and abscissae.""" @@ -85,13 +84,30 @@ class Quantity(models.Model): class MetaData(models.Model): """Database model for scattering metadata""" - # Associated data set - # dataset = models.OneToOneField( - # "DataSet", on_delete=models.CASCADE, related_name="associated_data" - # ) + # title + title = models.CharField(max_length=500, default="Title") + + # run + # TODO: find out if this is expected to be long + run = models.CharField(max_length=500, blank=True, null=True) + + # definition + definition = models.TextField(blank=True, null=True) + + # instrument + instrument = models.JSONField(blank=True, null=True) + + # process + process = models.JSONField(blank=True, null=True) + # sample + sample = models.JSONField(blank=True, null=True) -"""Database model for group of DataSets associated by a varying parameter.""" + # transmission spectrum + transmission_spectrum = models.JSONField(blank=True, null=True) + + # raw metadata (for recreating in SasView only) + raw_metadata = models.JSONField(default=dict) class OperationTree(models.Model): @@ -117,4 +133,15 @@ class Session(Data): # operation tree # operations = models.ForeignKey(OperationTree, on_delete=models.CASCADE) + +class PublishedState(): + """Database model for a project published state.""" + + # published + published = models.BooleanField(default=False) + + # doi + doi = models.URLField() + + ''' From 0ab84ec1c02d43073eb01a395ec7cd0b09711d06 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Mar 2025 11:20:51 -0500 Subject: [PATCH 122/129] Add to documentation on future ORCID integration --- sasdata/fair_database/fair_database/settings.py | 2 ++ sasdata/fair_database/user_app/urls.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sasdata/fair_database/fair_database/settings.py b/sasdata/fair_database/fair_database/settings.py index 885e3c90..6918a4de 100644 --- a/sasdata/fair_database/fair_database/settings.py +++ b/sasdata/fair_database/fair_database/settings.py @@ -113,6 +113,8 @@ ACCOUNT_EMAIL_VERIFICATION = "none" # to enable ORCID, register for credentials through ORCID and fill out client_id and secret +# https://info.orcid.org/documentation/integration-guide/ +# https://docs.allauth.org/en/latest/socialaccount/index.html SOCIALACCOUNT_PROVIDERS = { "orcid": { "APPS": [ diff --git a/sasdata/fair_database/user_app/urls.py b/sasdata/fair_database/user_app/urls.py index 808cbfce..cf44e8d6 100644 --- a/sasdata/fair_database/user_app/urls.py +++ b/sasdata/fair_database/user_app/urls.py @@ -1,8 +1,8 @@ from django.urls import path from dj_rest_auth.views import LogoutView, UserDetailsView, PasswordChangeView -from .views import KnoxLoginView, KnoxRegisterView, OrcidLoginView +from .views import KnoxLoginView, KnoxRegisterView -"""Urls for authentication. Orcid login not functional.""" +"""Urls for authentication. Orcid login not functional. See settings.py for ORCID activation.""" urlpatterns = [ path("register/", KnoxRegisterView.as_view(), name="register"), @@ -10,5 +10,5 @@ path("logout/", LogoutView.as_view(), name="logout"), path("user/", UserDetailsView.as_view(), name="view user information"), path("password/change/", PasswordChangeView.as_view(), name="change password"), - path("login/orcid/", OrcidLoginView.as_view(), name="orcid login"), + # path("login/orcid/", OrcidLoginView.as_view(), name="orcid login"), ] From 55d8ecf0cd7e77325b29081776f1aa6663cfe064 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Wed, 5 Mar 2025 13:21:30 -0500 Subject: [PATCH 123/129] Change data creation status code to 201 --- sasdata/fair_database/data/tests.py | 4 ++-- sasdata/fair_database/data/views.py | 5 ++++- sasdata/fair_database/fair_database/test_permissions.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index df15deaa..32b78b2b 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -92,7 +92,7 @@ def test_is_data_being_created(self): file = open(find("cyl_400_40.txt"), "rb") data = {"is_public": False, "file": file} request = self.client.post("/v1/data/upload/", data=data) - self.assertEqual(request.status_code, status.HTTP_200_OK) + self.assertEqual(request.status_code, status.HTTP_201_CREATED) self.assertEqual( request.data, { @@ -110,7 +110,7 @@ def test_is_data_being_created_no_user(self): file = open(find("cyl_400_40.txt"), "rb") data = {"is_public": True, "file": file} request = self.client2.post("/v1/data/upload/", data=data) - self.assertEqual(request.status_code, status.HTTP_200_OK) + self.assertEqual(request.status_code, status.HTTP_201_CREATED) self.assertEqual( request.data, { diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index 8bf48882..f28bc412 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -10,6 +10,7 @@ ) from rest_framework.decorators import api_view from rest_framework.response import Response +from rest_framework import status from sasdata.dataloader.loader import Loader from data.serializers import DataFileSerializer, AccessManagementSerializer @@ -58,7 +59,9 @@ def data_info(request, db_id, version=None): @api_view(["POST", "PUT"]) def upload(request, data_id=None, version=None): # saves file + response_status = status.HTTP_200_OK if request.method in ["POST", "PUT"] and data_id is None: + response_status = status.HTTP_201_CREATED form = DataFileForm(request.data, request.FILES) if form.is_valid(): form.save() @@ -115,7 +118,7 @@ def upload(request, data_id=None, version=None): "file_alternative_name": serializer.data["file_name"], "is_public": serializer.data["is_public"], } - return Response(return_data) + return Response(return_data, status=response_status) # view or control who has access to a file diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index 9bf91d04..09aef326 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -140,7 +140,7 @@ def test_upload_authenticated(self): response = self.client.post( "/v1/data/upload/", data=data, headers=auth_header(token) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual( response.data, { @@ -161,7 +161,7 @@ def test_upload_unauthenticated(self): data2 = {"file": file2, "is_public": False} response = self.client.post("/v1/data/upload/", data=data) response2 = self.client.post("/v1/data/upload/", data=data2) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual( response.data, { From fd9c27a166f9b37245502dfa40f993c94c25f9d5 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Mar 2025 11:25:35 -0500 Subject: [PATCH 124/129] Modify tests to run faster --- .../fair_database/test_permissions.py | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/sasdata/fair_database/fair_database/test_permissions.py b/sasdata/fair_database/fair_database/test_permissions.py index 09aef326..c02a5e78 100644 --- a/sasdata/fair_database/fair_database/test_permissions.py +++ b/sasdata/fair_database/fair_database/test_permissions.py @@ -22,37 +22,38 @@ def auth_header(response): class DataListPermissionsTests(APITestCase): """Test permissions of data views using user_app for authentication.""" - def setUp(self): - self.user = User.objects.create_user( + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( username="testUser", password="secret", id=1, email="email@domain.com" ) - self.user2 = User.objects.create_user( + cls.user2 = User.objects.create_user( username="testUser2", password="secret", id=2, email="email2@domain.com" ) - unowned_test_data = DataFile.objects.create( + cls.unowned_test_data = DataFile.objects.create( id=1, file_name="cyl_400_40.txt", is_public=True ) - unowned_test_data.file.save( + cls.unowned_test_data.file.save( "cyl_400_40.txt", open(find("cyl_400_40.txt"), "rb") ) - private_test_data = DataFile.objects.create( - id=2, current_user=self.user, file_name="cyl_400_20.txt", is_public=False + cls.private_test_data = DataFile.objects.create( + id=2, current_user=cls.user, file_name="cyl_400_20.txt", is_public=False ) - private_test_data.file.save( + cls.private_test_data.file.save( "cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb") ) - public_test_data = DataFile.objects.create( - id=3, current_user=self.user, file_name="cyl_testdata.txt", is_public=True + cls.public_test_data = DataFile.objects.create( + id=3, current_user=cls.user, file_name="cyl_testdata.txt", is_public=True ) - public_test_data.file.save( + cls.public_test_data.file.save( "cyl_testdata.txt", open(find("cyl_testdata.txt"), "rb") ) - self.login_data_1 = { + cls.login_data_1 = { "username": "testUser", "password": "secret", "email": "email@domain.com", } - self.login_data_2 = { + cls.login_data_2 = { "username": "testUser2", "password": "secret", "email": "email2@domain.com", @@ -268,5 +269,11 @@ def test_download_unauthenticated(self): self.assertEqual(response2.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response3.status_code, status.HTTP_200_OK) - def tearDown(self): + @classmethod + def tearDownClass(cls): + cls.user.delete() + cls.user2.delete() + cls.public_test_data.delete() + cls.private_test_data.delete() + cls.unowned_test_data.delete() shutil.rmtree(settings.MEDIA_ROOT) From e3904d2d4191221fd0b57ef911167af8911863a2 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Mar 2025 11:40:59 -0500 Subject: [PATCH 125/129] Change authentication tests to run faster --- sasdata/fair_database/user_app/tests.py | 112 ++++++++++++------------ 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index 664fd963..f5dd6b04 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -7,98 +7,96 @@ # Create your tests here. class AuthTests(TestCase): - def setUp(self): - self.client = APIClient() - self.register_data = { + @classmethod + def setUpTestData(cls): + cls.client1 = APIClient() + cls.register_data = { "email": "email@domain.org", "username": "testUser", "password1": "sasview!", "password2": "sasview!", } - self.login_data = { + cls.login_data = { "username": "testUser", "email": "email@domain.org", "password": "sasview!", } + cls.login_data_2 = { + "username": "testUser2", + "email": "email2@domain.org", + "password": "sasview!", + } + cls.user = User.objects.create_user( + id=1, username="testUser2", password="sasview!", email="email2@domain.org" + ) def auth_header(self, response): return {"Authorization": "Token " + response.data["token"]} # Test if registration successfully creates a new user and logs in def test_register(self): - response = self.client.post("/auth/register/", data=self.register_data) + response = self.client1.post("/auth/register/", data=self.register_data) user = User.objects.get(username="testUser") - response2 = self.client.get("/auth/user/", headers=self.auth_header(response)) + response2 = self.client1.get("/auth/user/", headers=self.auth_header(response)) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(user.email, self.register_data["email"]) self.assertEqual(response2.status_code, status.HTTP_200_OK) + user.delete() # Test if login successful def test_login(self): - User.objects.create_user( - username="testUser", password="sasview!", email="email@domain.org" - ) - response = self.client.post("/auth/login/", data=self.login_data) - response2 = self.client.get("/auth/user/", headers=self.auth_header(response)) + response = self.client1.post("/auth/login/", data=self.login_data_2) + response2 = self.client1.get("/auth/user/", headers=self.auth_header(response)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) # Test simultaneous login by multiple clients def test_multiple_login(self): - User.objects.create_user( - username="testUser", password="sasview!", email="email@domain.org" - ) client2 = APIClient() - response = self.client.post("/auth/login/", data=self.login_data) - response2 = client2.post("/auth/login/", data=self.login_data) + response = self.client1.post("/auth/login/", data=self.login_data_2) + response2 = client2.post("/auth/login/", data=self.login_data_2) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) self.assertNotEqual(response.content, response2.content) # Test get user information def test_user_get(self): - user = User.objects.create_user( - username="testUser", password="sasview!", email="email@domain.org" - ) - self.client.force_authenticate(user=user) - response = self.client.get("/auth/user/") + self.client1.force_authenticate(user=self.user) + response = self.client1.get("/auth/user/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.content, - b'{"pk":1,"username":"testUser","email":"email@domain.org","first_name":"","last_name":""}', + b'{"pk":1,"username":"testUser2","email":"email2@domain.org","first_name":"","last_name":""}', ) # Test changing username def test_user_put_username(self): - user = User.objects.create_user( - username="testUser", password="sasview!", email="email@domain.org" - ) - self.client.force_authenticate(user=user) + self.client1.force_authenticate(user=self.user) data = {"username": "newName"} - response = self.client.put("/auth/user/", data=data) + response = self.client1.put("/auth/user/", data=data) + self.user.username = "testUser2" self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.content, - b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"","last_name":""}', + b'{"pk":1,"username":"newName","email":"email2@domain.org","first_name":"","last_name":""}', ) # Test changing username and first and last name def test_user_put_name(self): - user = User.objects.create_user( - username="testUser", password="sasview!", email="email@domain.org" - ) - self.client.force_authenticate(user=user) + self.client1.force_authenticate(user=self.user) data = {"username": "newName", "first_name": "Clark", "last_name": "Kent"} - response = self.client.put("/auth/user/", data=data) + response = self.client1.put("/auth/user/", data=data) + self.user.first_name = "" + self.user.last_name = "" self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.content, - b'{"pk":1,"username":"newName","email":"email@domain.org","first_name":"Clark","last_name":"Kent"}', + b'{"pk":1,"username":"newName","email":"email2@domain.org","first_name":"Clark","last_name":"Kent"}', ) # Test user info inaccessible when unauthenticated def test_user_unauthenticated(self): - response = self.client.get("/auth/user/") + response = self.client1.get("/auth/user/") self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual( response.content, @@ -107,33 +105,28 @@ def test_user_unauthenticated(self): # Test logout is successful after login def test_login_logout(self): - User.objects.create_user( - username="testUser", password="sasview!", email="email@domain.org" - ) - self.client.post("/auth/login/", data=self.login_data) - response = self.client.post("/auth/logout/") - response2 = self.client.get("/auth/user/") + self.client1.post("/auth/login/", data=self.login_data_2) + response = self.client1.post("/auth/logout/") + response2 = self.client1.get("/auth/user/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEqual(response2.status_code, status.HTTP_401_UNAUTHORIZED) # Test logout is successful after registration def test_register_logout(self): - self.client.post("/auth/register/", data=self.register_data) - response = self.client.post("/auth/logout/") - response2 = self.client.get("/auth/user/") + self.client1.post("/auth/register/", data=self.register_data) + response = self.client1.post("/auth/logout/") + response2 = self.client1.get("/auth/user/") + User.objects.get(username="testUser").delete() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.content, b'{"detail":"Successfully logged out."}') self.assertEqual(response2.status_code, status.HTTP_401_UNAUTHORIZED) def test_multiple_logout(self): - User.objects.create_user( - username="testUser", password="sasview!", email="email@domain.org" - ) client2 = APIClient() - self.client.post("/auth/login/", data=self.login_data) - token = client2.post("/auth/login/", data=self.login_data) - response = self.client.post("/auth/logout/") + self.client1.post("/auth/login/", data=self.login_data_2) + token = client2.post("/auth/login/", data=self.login_data_2) + response = self.client1.post("/auth/logout/") response2 = client2.get("/auth/user/", headers=self.auth_header(token)) response3 = client2.post("/auth/logout/") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -142,16 +135,19 @@ def test_multiple_logout(self): # Test login is successful after registering then logging out def test_register_login(self): - register_response = self.client.post("/auth/register/", data=self.register_data) - logout_response = self.client.post("/auth/logout/") - login_response = self.client.post("/auth/login/", data=self.login_data) + register_response = self.client1.post( + "/auth/register/", data=self.register_data + ) + logout_response = self.client1.post("/auth/logout/") + login_response = self.client1.post("/auth/login/", data=self.login_data) + User.objects.get(username="testUser").delete() self.assertEqual(register_response.status_code, status.HTTP_201_CREATED) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) # Test password is successfully changed def test_password_change(self): - token = self.client.post("/auth/register/", data=self.register_data) + token = self.client1.post("/auth/register/", data=self.register_data) data = { "new_password1": "sasview?", "new_password2": "sasview?", @@ -159,11 +155,13 @@ def test_password_change(self): } l_data = self.login_data l_data["password"] = "sasview?" - response = self.client.post( + response = self.client1.post( "/auth/password/change/", data=data, headers=self.auth_header(token) ) - logout_response = self.client.post("/auth/logout/") - login_response = self.client.post("/auth/login/", data=l_data) + logout_response = self.client1.post("/auth/logout/") + login_response = self.client1.post("/auth/login/", data=l_data) + l_data["password"] = "sasview!" + User.objects.get(username="testUser").delete() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) From 8dc97973857bb9448cec0e0e58fff86ce4567f8f Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Mar 2025 13:51:21 -0500 Subject: [PATCH 126/129] Speed up data tests --- sasdata/fair_database/data/tests.py | 148 ++++++++++++++++------------ 1 file changed, 85 insertions(+), 63 deletions(-) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 32b78b2b..00959218 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -3,6 +3,7 @@ from django.conf import settings from django.test import TestCase +from django.db.models import Max from django.contrib.auth.models import User from rest_framework.test import APIClient, APITestCase from rest_framework import status @@ -17,31 +18,34 @@ def find(filename): class TestLists(TestCase): - def setUp(self): - public_test_data = DataFile.objects.create( + @classmethod + def setUpTestData(cls): + cls.public_test_data = DataFile.objects.create( id=1, file_name="cyl_400_40.txt", is_public=True ) - public_test_data.file.save("cyl_400_40.txt", open(find("cyl_400_40.txt"), "rb")) - self.user = User.objects.create_user( + cls.public_test_data.file.save( + "cyl_400_40.txt", open(find("cyl_400_40.txt"), "rb") + ) + cls.user = User.objects.create_user( username="testUser", password="secret", id=2 ) - private_test_data = DataFile.objects.create( - id=3, current_user=self.user, file_name="cyl_400_20.txt", is_public=False + cls.private_test_data = DataFile.objects.create( + id=3, current_user=cls.user, file_name="cyl_400_20.txt", is_public=False ) - private_test_data.file.save( + cls.private_test_data.file.save( "cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb") ) - self.client = APIClient() - self.client.force_authenticate(user=self.user) + cls.client1 = APIClient() + cls.client1.force_authenticate(user=cls.user) # Test list public data def test_does_list_public(self): - request = self.client.get("/v1/data/list/") + request = self.client1.get("/v1/data/list/") self.assertEqual(request.data, {"public_data_ids": {1: "cyl_400_40.txt"}}) # Test list a user's private data def test_does_list_user(self): - request = self.client.get("/v1/data/list/testUser/", user=self.user) + request = self.client1.get("/v1/data/list/testUser/", user=self.user) self.assertEqual(request.data, {"user_data_ids": {3: "cyl_400_20.txt"}}) # Test list another user's public data @@ -52,122 +56,131 @@ def test_list_other_user(self): # Test list a nonexistent user's data def test_list_nonexistent_user(self): - request = self.client.get("/v1/data/list/fakeUser/") + request = self.client1.get("/v1/data/list/fakeUser/") self.assertEqual(request.status_code, status.HTTP_404_NOT_FOUND) # Test loading a public data file def test_does_load_data_info_public(self): - request = self.client.get("/v1/data/load/1/") + request = self.client1.get("/v1/data/load/1/") self.assertEqual(request.status_code, status.HTTP_200_OK) # Test loading private data with authorization def test_does_load_data_info_private(self): - request = self.client.get("/v1/data/load/3/") + request = self.client1.get("/v1/data/load/3/") self.assertEqual(request.status_code, status.HTTP_200_OK) # Test loading data that does not exist def test_load_data_info_nonexistent(self): - request = self.client.get("/v1/data/load/5/") + request = self.client1.get("/v1/data/load/5/") self.assertEqual(request.status_code, status.HTTP_404_NOT_FOUND) - def tearDown(self): + @classmethod + def tearDownClass(cls): + cls.public_test_data.delete() + cls.private_test_data.delete() + cls.user.delete() shutil.rmtree(settings.MEDIA_ROOT) class TestingDatabase(APITestCase): - def setUp(self): - self.user = User.objects.create_user( + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( username="testUser", password="secret", id=1 ) - self.data = DataFile.objects.create( - id=2, current_user=self.user, file_name="cyl_400_20.txt", is_public=False + cls.data = DataFile.objects.create( + id=1, current_user=cls.user, file_name="cyl_400_20.txt", is_public=False ) - self.data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb")) - self.client = APIClient() - self.client.force_authenticate(user=self.user) - self.client2 = APIClient() + cls.data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb")) + cls.client1 = APIClient() + cls.client1.force_authenticate(user=cls.user) + cls.client2 = APIClient() # Test data upload creates data in database def test_is_data_being_created(self): file = open(find("cyl_400_40.txt"), "rb") data = {"is_public": False, "file": file} - request = self.client.post("/v1/data/upload/", data=data) + request = self.client1.post("/v1/data/upload/", data=data) + max_id = DataFile.objects.aggregate(Max("id"))["id__max"] self.assertEqual(request.status_code, status.HTTP_201_CREATED) self.assertEqual( request.data, { "current_user": "testUser", "authenticated": True, - "file_id": 3, + "file_id": max_id, "file_alternative_name": "cyl_400_40.txt", "is_public": False, }, ) - DataFile.objects.get(id=3).delete() + DataFile.objects.get(id=max_id).delete() # Test data upload w/out authenticated user def test_is_data_being_created_no_user(self): - file = open(find("cyl_400_40.txt"), "rb") + file = open(find("cyl_testdata.txt"), "rb") data = {"is_public": True, "file": file} request = self.client2.post("/v1/data/upload/", data=data) + max_id = DataFile.objects.aggregate(Max("id"))["id__max"] self.assertEqual(request.status_code, status.HTTP_201_CREATED) self.assertEqual( request.data, { "current_user": "", "authenticated": False, - "file_id": 3, - "file_alternative_name": "cyl_400_40.txt", + "file_id": max_id, + "file_alternative_name": "cyl_testdata.txt", "is_public": True, }, ) - DataFile.objects.get(id=3).delete() + DataFile.objects.get(id=max_id).delete() # Test updating file def test_does_file_upload_update(self): - file = open(find("cyl_400_40.txt")) + file = open(find("cyl_testdata1.txt")) data = {"file": file, "is_public": False} - request = self.client.put("/v1/data/upload/2/", data=data) + request = self.client1.put("/v1/data/upload/1/", data=data) self.assertEqual( request.data, { "current_user": "testUser", "authenticated": True, - "file_id": 2, - "file_alternative_name": "cyl_400_40.txt", + "file_id": 1, + "file_alternative_name": "cyl_testdata1.txt", "is_public": False, }, ) - DataFile.objects.get(id=2).delete() + self.data.file.save("cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb")) + self.data.file_name = "cyl_400_20.txt" # Test updating a public file def test_public_file_upload_update(self): data_object = DataFile.objects.create( - id=3, current_user=self.user, file_name="cyl_testdata.txt", is_public=True + id=3, current_user=self.user, file_name="cyl_testdata2.txt", is_public=True ) - data_object.file.save("cyl_testdata.txt", open(find("cyl_testdata.txt"), "rb")) - file = open(find("cyl_testdata1.txt")) + data_object.file.save( + "cyl_testdata2.txt", open(find("cyl_testdata2.txt"), "rb") + ) + file = open(find("conalbumin.txt")) data = {"file": file, "is_public": True} - request = self.client.put("/v1/data/upload/3/", data=data) + request = self.client1.put("/v1/data/upload/3/", data=data) self.assertEqual( request.data, { "current_user": "testUser", "authenticated": True, "file_id": 3, - "file_alternative_name": "cyl_testdata1.txt", + "file_alternative_name": "conalbumin.txt", "is_public": True, }, ) - DataFile.objects.get(id=3).delete() + data_object.delete() # Test file upload update fails when unauthorized def test_unauthorized_file_upload_update(self): file = open(find("cyl_400_40.txt")) data = {"file": file, "is_public": False} - request = self.client2.put("/v1/data/upload/2/", data=data) + request = self.client2.put("/v1/data/upload/1/", data=data) self.assertEqual(request.status_code, status.HTTP_403_FORBIDDEN) - DataFile.objects.get(id=2).delete() # Test update nonexistent file fails def test_file_upload_update_not_found(self): @@ -178,7 +191,7 @@ def test_file_upload_update_not_found(self): # Test file download def test_does_download(self): - request = self.client.get("/v1/data/2/download/") + request = self.client1.get("/v1/data/1/download/") file_contents = b"".join(request.streaming_content) test_file = open(find("cyl_400_20.txt"), "rb") self.assertEqual(request.status_code, status.HTTP_200_OK) @@ -186,39 +199,43 @@ def test_does_download(self): # Test file download fails when unauthorized def test_unauthorized_download(self): - request2 = self.client2.get("/v1/data/2/download/") + request2 = self.client2.get("/v1/data/1/download/") self.assertEqual(request2.status_code, status.HTTP_403_FORBIDDEN) # Test download nonexistent file def test_download_nonexistent(self): - request = self.client.get("/v1/data/5/download/") + request = self.client1.get("/v1/data/5/download/") self.assertEqual(request.status_code, status.HTTP_404_NOT_FOUND) - def tearDown(self): + @classmethod + def tearDownClass(cls): + cls.user.delete() + cls.data.delete() shutil.rmtree(settings.MEDIA_ROOT) class TestAccessManagement(TestCase): - def setUp(self): - self.user1 = User.objects.create_user(username="testUser", password="secret") - self.user2 = User.objects.create_user(username="testUser2", password="secret2") - self.private_test_data = DataFile.objects.create( - id=1, current_user=self.user1, file_name="cyl_400_40.txt", is_public=False + @classmethod + def setUpTestData(cls): + cls.user1 = User.objects.create_user(username="testUser", password="secret") + cls.user2 = User.objects.create_user(username="testUser2", password="secret2") + cls.private_test_data = DataFile.objects.create( + id=1, current_user=cls.user1, file_name="cyl_400_40.txt", is_public=False ) - self.private_test_data.file.save( + cls.private_test_data.file.save( "cyl_400_40.txt", open(find("cyl_400_40.txt"), "rb") ) - self.shared_test_data = DataFile.objects.create( - id=2, current_user=self.user1, file_name="cyl_400_20.txt", is_public=False + cls.shared_test_data = DataFile.objects.create( + id=2, current_user=cls.user1, file_name="cyl_400_20.txt", is_public=False ) - self.shared_test_data.file.save( + cls.shared_test_data.file.save( "cyl_400_20.txt", open(find("cyl_400_20.txt"), "rb") ) - self.shared_test_data.users.add(self.user2) - self.client1 = APIClient() - self.client1.force_authenticate(self.user1) - self.client2 = APIClient() - self.client2.force_authenticate(self.user2) + cls.shared_test_data.users.add(cls.user2) + cls.client1 = APIClient() + cls.client1.force_authenticate(cls.user1) + cls.client2 = APIClient() + cls.client2.force_authenticate(cls.user2) # test viewing no one with access def test_view_no_access(self): @@ -301,5 +318,10 @@ def test_only_edit_access_to_owned_file(self): self.assertEqual(request3.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(request4.status_code, status.HTTP_200_OK) - def tearDown(self): + @classmethod + def tearDownClass(cls): + cls.user1.delete() + cls.user2.delete() + cls.private_test_data.delete() + cls.shared_test_data.delete() shutil.rmtree(settings.MEDIA_ROOT) From 8a0a672ac5535a15f50ff4e6e2ed237d589e88c5 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Mar 2025 15:28:35 -0500 Subject: [PATCH 127/129] One more attempt to speed up user_app tests --- sasdata/fair_database/user_app/tests.py | 37 ++++++++++--------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/sasdata/fair_database/user_app/tests.py b/sasdata/fair_database/user_app/tests.py index f5dd6b04..f53e922c 100644 --- a/sasdata/fair_database/user_app/tests.py +++ b/sasdata/fair_database/user_app/tests.py @@ -10,6 +10,7 @@ class AuthTests(TestCase): @classmethod def setUpTestData(cls): cls.client1 = APIClient() + cls.client2 = APIClient() cls.register_data = { "email": "email@domain.org", "username": "testUser", @@ -29,6 +30,8 @@ def setUpTestData(cls): cls.user = User.objects.create_user( id=1, username="testUser2", password="sasview!", email="email2@domain.org" ) + cls.client3 = APIClient() + cls.client3.force_authenticate(user=cls.user) def auth_header(self, response): return {"Authorization": "Token " + response.data["token"]} @@ -52,17 +55,15 @@ def test_login(self): # Test simultaneous login by multiple clients def test_multiple_login(self): - client2 = APIClient() response = self.client1.post("/auth/login/", data=self.login_data_2) - response2 = client2.post("/auth/login/", data=self.login_data_2) + response2 = self.client2.post("/auth/login/", data=self.login_data_2) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) self.assertNotEqual(response.content, response2.content) # Test get user information def test_user_get(self): - self.client1.force_authenticate(user=self.user) - response = self.client1.get("/auth/user/") + response = self.client3.get("/auth/user/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.content, @@ -71,9 +72,8 @@ def test_user_get(self): # Test changing username def test_user_put_username(self): - self.client1.force_authenticate(user=self.user) data = {"username": "newName"} - response = self.client1.put("/auth/user/", data=data) + response = self.client3.put("/auth/user/", data=data) self.user.username = "testUser2" self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( @@ -83,9 +83,8 @@ def test_user_put_username(self): # Test changing username and first and last name def test_user_put_name(self): - self.client1.force_authenticate(user=self.user) data = {"username": "newName", "first_name": "Clark", "last_name": "Kent"} - response = self.client1.put("/auth/user/", data=data) + response = self.client3.put("/auth/user/", data=data) self.user.first_name = "" self.user.last_name = "" self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -123,12 +122,11 @@ def test_register_logout(self): self.assertEqual(response2.status_code, status.HTTP_401_UNAUTHORIZED) def test_multiple_logout(self): - client2 = APIClient() self.client1.post("/auth/login/", data=self.login_data_2) - token = client2.post("/auth/login/", data=self.login_data_2) + token = self.client2.post("/auth/login/", data=self.login_data_2) response = self.client1.post("/auth/logout/") - response2 = client2.get("/auth/user/", headers=self.auth_header(token)) - response3 = client2.post("/auth/logout/") + response2 = self.client2.get("/auth/user/", headers=self.auth_header(token)) + response3 = self.client2.post("/auth/logout/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response2.status_code, status.HTTP_200_OK) self.assertEqual(response3.status_code, status.HTTP_200_OK) @@ -147,23 +145,16 @@ def test_register_login(self): # Test password is successfully changed def test_password_change(self): - token = self.client1.post("/auth/register/", data=self.register_data) data = { "new_password1": "sasview?", "new_password2": "sasview?", "old_password": "sasview!", } - l_data = self.login_data - l_data["password"] = "sasview?" - response = self.client1.post( - "/auth/password/change/", data=data, headers=self.auth_header(token) - ) - logout_response = self.client1.post("/auth/logout/") - login_response = self.client1.post("/auth/login/", data=l_data) - l_data["password"] = "sasview!" - User.objects.get(username="testUser").delete() + self.login_data_2["password"] = "sasview?" + response = self.client3.post("/auth/password/change/", data=data) + login_response = self.client1.post("/auth/login/", data=self.login_data_2) + self.login_data_2["password"] = "sasview!" self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(logout_response.status_code, status.HTTP_200_OK) self.assertEqual(login_response.status_code, status.HTTP_200_OK) From 7a3aedd42495dea5597cc723ab9d500114588ae2 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Mar 2025 15:37:39 -0500 Subject: [PATCH 128/129] Create view to delete a file --- sasdata/fair_database/data/urls.py | 1 + sasdata/fair_database/data/views.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/sasdata/fair_database/data/urls.py b/sasdata/fair_database/data/urls.py index 8721927f..a61fff55 100644 --- a/sasdata/fair_database/data/urls.py +++ b/sasdata/fair_database/data/urls.py @@ -10,4 +10,5 @@ path("upload/<data_id>/", views.upload, name="update file in data"), path("<int:data_id>/download/", views.download, name="download data from db"), path("manage/<int:data_id>/", views.manage_access, name="manage access to files"), + path("delete/<int:data_id>/", views.delete, name="delete file"), ] diff --git a/sasdata/fair_database/data/views.py b/sasdata/fair_database/data/views.py index f28bc412..73aba65c 100644 --- a/sasdata/fair_database/data/views.py +++ b/sasdata/fair_database/data/views.py @@ -152,6 +152,16 @@ def manage_access(request, data_id, version=None): return HttpResponseBadRequest() +# delete a file +@api_view(["DELETE"]) +def delete(request, data_id, version=None): + db = get_object_or_404(DataFile, id=data_id) + if not permissions.is_owner(request, db): + return HttpResponseForbidden("Must be the data owner to delete") + db.delete() + return Response(data={"success": True}) + + # downloads a file @api_view(["GET"]) def download(request, data_id, version=None): From c039a29224765d9c0c1645922dd82f73c8477fb7 Mon Sep 17 00:00:00 2001 From: summerhenson <summerh@gmail.com> Date: Thu, 6 Mar 2025 15:55:31 -0500 Subject: [PATCH 129/129] Tests for file delete --- sasdata/fair_database/data/tests.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sasdata/fair_database/data/tests.py b/sasdata/fair_database/data/tests.py index 00959218..b5308967 100644 --- a/sasdata/fair_database/data/tests.py +++ b/sasdata/fair_database/data/tests.py @@ -207,6 +207,20 @@ def test_download_nonexistent(self): request = self.client1.get("/v1/data/5/download/") self.assertEqual(request.status_code, status.HTTP_404_NOT_FOUND) + # Test deleting a file + def test_delete(self): + DataFile.objects.create( + id=6, current_user=self.user, file_name="test.txt", is_public=False + ) + request = self.client1.delete("/v1/data/delete/6/") + self.assertEqual(request.status_code, status.HTTP_200_OK) + self.assertFalse(DataFile.objects.filter(pk=6).exists()) + + # Test deleting a file fails when unauthorized + def test_delete_unauthorized(self): + request = self.client2.delete("/v1/data/delete/1/") + self.assertEqual(request.status_code, status.HTTP_403_FORBIDDEN) + @classmethod def tearDownClass(cls): cls.user.delete()