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()